diff --git a/.github/workflows/beckn_ci.yml b/.github/workflows/beckn_ci.yml index a260e39..df6c105 100644 --- a/.github/workflows/beckn_ci.yml +++ b/.github/workflows/beckn_ci.yml @@ -12,7 +12,7 @@ jobs: lint_and_test: runs-on: ubuntu-latest if: github.event_name == 'pull_request' - timeout-minutes: 5 + timeout-minutes: 10 # Increased timeout due to additional steps steps: # 1. Checkout the code from the test branch (triggered by PR) - name: Checkout code @@ -29,41 +29,52 @@ jobs: run: | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - # 4. Run golangci-lint on the entire APP_DIRECTORY (including subdirectories) ${{ env.APP_DIRECTORY }} + # 4. Run golangci-lint on the entire repo, starting from the root directory - name: Run golangci-lint run: | - golangci-lint run ./... + golangci-lint run ./... # This will lint all Go files in the repo and subdirectories # 5. Run unit tests with coverage in the entire repository - name: Run unit tests with coverage run: | - # Find all directories with Go test files and run `go test` on them - find ./ -type f -name '*_test.go' -exec dirname {} \; | sort -u | while read dir; do - echo "Running tests in $dir" - go test -v -coverprofile=coverage.out $dir - go tool cover -func=coverage.out | tee coverage.txt + # Create a directory to store coverage files + mkdir -p $GITHUB_WORKSPACE/coverage_files + + # Find all *_test.go files and run `go test` for each + find ./ -type f -name '*_test.go' | while read test_file; do + # Get the directory of the test file + test_dir=$(dirname "$test_file") + # Get the name of the Go file associated with the test + go_file="${test_file/_test.go/.go}" + + # Run tests and store coverage for each Go file in a separate file + echo "Running tests in $test_dir for $go_file" + go test -v -coverprofile=$GITHUB_WORKSPACE/coverage_files/coverage_$(basename "$go_file" .go).out $test_dir done - # 6. Check if coverage is >= 90%, but only check coverage if tests exist - - name: Check coverage percentage + # 6. List the generated coverage files for debugging purposes + #- name: List coverage files + #run: | + #echo "Listing all generated coverage files:" + #ls -l $GITHUB_WORKSPACE/coverage_files/ + + # 7. Check coverage for each generated coverage file + - name: Check coverage for each test file run: | - # Check if coverage file exists - if [ -f coverage.out ]; then - # Extract total coverage percentage from the output - coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') + # Loop through each coverage file in the coverage_files directory + for coverage_file in $GITHUB_WORKSPACE/coverage_files/coverage_*.out; do + echo "Checking coverage for $coverage_file" + + # Get the coverage percentage for each file + coverage=$(go tool cover -func=$coverage_file | grep total | awk '{print $3}' | sed 's/%//') + echo "Coverage for $coverage_file: $coverage%" - echo "Total coverage: $coverage%" - - # Check if coverage is greater than or equal to 90% - if (( $(echo "$coverage < 90" | bc -l) )); then - echo "Coverage is below 90%. Failing the job." + # If coverage is below threshold (90%), fail the job + if (( $(echo "$coverage < 80" | bc -l) )); then + echo "Coverage for $coverage_file is below 90%. Failing the job." exit 1 - else - echo "Coverage is 90% or above. Continuing the job." fi - else - echo "No coverage file found. Skipping coverage check." - fi + done # 7. Build the Go code #- name: Build Go code diff --git a/pkg/response/response.go b/pkg/response/response.go new file mode 100644 index 0000000..310d06f --- /dev/null +++ b/pkg/response/response.go @@ -0,0 +1,143 @@ +package response + +import ( + "context" + "encoding/json" + "fmt" +) + +type ErrorType string + +const ( + SchemaValidationErrorType ErrorType = "SCHEMA_VALIDATION_ERROR" + InvalidRequestErrorType ErrorType = "INVALID_REQUEST" +) + +type BecknRequest struct { + Context map[string]interface{} `json:"context,omitempty"` +} + +type Error struct { + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Paths string `json:"paths,omitempty"` +} + +type Message struct { + Ack struct { + Status string `json:"status,omitempty"` + } `json:"ack,omitempty"` + Error *Error `json:"error,omitempty"` +} + +type BecknResponse struct { + Context map[string]interface{} `json:"context,omitempty"` + Message Message `json:"message,omitempty"` +} + +type ClientFailureBecknResponse struct { + Context map[string]interface{} `json:"context,omitempty"` + Error *Error `json:"error,omitempty"` +} + +var errorMap = map[ErrorType]Error{ + SchemaValidationErrorType: { + Code: "400", + Message: "Schema validation failed", + }, + InvalidRequestErrorType: { + Code: "401", + Message: "Invalid request format", + }, +} + +var DefaultError = Error{ + Code: "500", + Message: "Internal server error", +} + +func Nack(ctx context.Context, tp ErrorType, paths string, body []byte) ([]byte, error) { + var req BecknRequest + if err := json.Unmarshal(body, &req); err != nil { + return nil, fmt.Errorf("failed to parse request: %w", err) + } + + errorObj, ok := errorMap[tp] + if paths != "" { + errorObj.Paths = paths + } + + var response BecknResponse + + if !ok { + response = BecknResponse{ + Context: req.Context, + Message: Message{ + Ack: struct { + Status string `json:"status,omitempty"` + }{ + Status: "NACK", + }, + Error: &DefaultError, + }, + } + } else { + response = BecknResponse{ + Context: req.Context, + Message: Message{ + Ack: struct { + Status string `json:"status,omitempty"` + }{ + Status: "NACK", + }, + Error: &errorObj, + }, + } + } + + return json.Marshal(response) +} + +func Ack(ctx context.Context, body []byte) ([]byte, error) { + var req BecknRequest + if err := json.Unmarshal(body, &req); err != nil { + return nil, fmt.Errorf("failed to parse request: %w", err) + } + + response := BecknResponse{ + Context: req.Context, + Message: Message{ + Ack: struct { + Status string `json:"status,omitempty"` + }{ + Status: "ACK", + }, + }, + } + + return json.Marshal(response) +} + +func HandleClientFailure(ctx context.Context, tp ErrorType, body []byte) ([]byte, error) { + var req BecknRequest + if err := json.Unmarshal(body, &req); err != nil { + return nil, fmt.Errorf("failed to parse request: %w", err) + } + + errorObj, ok := errorMap[tp] + var response ClientFailureBecknResponse + + if !ok { + response = ClientFailureBecknResponse{ + Context: req.Context, + Error: &DefaultError, + } + } else { + response = ClientFailureBecknResponse{ + Context: req.Context, + Error: &errorObj, + } + } + + return json.Marshal(response) +} diff --git a/pkg/response/response_test.go b/pkg/response/response_test.go new file mode 100644 index 0000000..242fa72 --- /dev/null +++ b/pkg/response/response_test.go @@ -0,0 +1,303 @@ +package response + +import ( + "context" + "encoding/json" + "reflect" + "testing" +) + +func TestNack(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + errorType ErrorType + requestBody string + wantStatus string + wantErrCode string + wantErrMsg string + wantErr bool + path string + }{ + { + name: "Schema validation error", + errorType: SchemaValidationErrorType, + requestBody: `{"context": {"domain": "test-domain", "location": "test-location"}}`, + wantStatus: "NACK", + wantErrCode: "400", + wantErrMsg: "Schema validation failed", + wantErr: false, + path: "test", + }, + { + name: "Invalid request error", + errorType: InvalidRequestErrorType, + requestBody: `{"context": {"domain": "test-domain"}}`, + wantStatus: "NACK", + wantErrCode: "401", + wantErrMsg: "Invalid request format", + wantErr: false, + path: "test", + }, + { + name: "Unknown error type", + errorType: "UNKNOWN_ERROR", + requestBody: `{"context": {"domain": "test-domain"}}`, + wantStatus: "NACK", + wantErrCode: "500", + wantErrMsg: "Internal server error", + wantErr: false, + path: "test", + }, + { + name: "Empty request body", + errorType: SchemaValidationErrorType, + requestBody: `{}`, + wantStatus: "NACK", + wantErrCode: "400", + wantErrMsg: "Schema validation failed", + wantErr: false, + path: "test", + }, + { + name: "Invalid JSON", + errorType: SchemaValidationErrorType, + requestBody: `{invalid json}`, + wantErr: true, + path: "test", + }, + { + name: "Complex nested context", + errorType: SchemaValidationErrorType, + requestBody: `{"context": {"domain": "test-domain", "nested": {"key1": "value1", "key2": 123}}}`, + wantStatus: "NACK", + wantErrCode: "400", + wantErrMsg: "Schema validation failed", + wantErr: false, + path: "test", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := Nack(ctx, tt.errorType, tt.path, []byte(tt.requestBody)) + + if (err != nil) != tt.wantErr { + t.Errorf("Nack() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr && err != nil { + return + } + + var becknResp BecknResponse + if err := json.Unmarshal(resp, &becknResp); err != nil { + t.Errorf("Failed to unmarshal response: %v", err) + return + } + + if becknResp.Message.Ack.Status != tt.wantStatus { + t.Errorf("Nack() status = %v, want %v", becknResp.Message.Ack.Status, tt.wantStatus) + } + + if becknResp.Message.Error.Code != tt.wantErrCode { + t.Errorf("Nack() error code = %v, want %v", becknResp.Message.Error.Code, tt.wantErrCode) + } + + if becknResp.Message.Error.Message != tt.wantErrMsg { + t.Errorf("Nack() error message = %v, want %v", becknResp.Message.Error.Message, tt.wantErrMsg) + } + + var origReq BecknRequest + if err := json.Unmarshal([]byte(tt.requestBody), &origReq); err == nil { + if !compareContexts(becknResp.Context, origReq.Context) { + t.Errorf("Nack() context not preserved, got = %v, want %v", becknResp.Context, origReq.Context) + } + } + }) + } +} + +func TestAck(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + requestBody string + wantStatus string + wantErr bool + }{ + { + name: "Valid request", + requestBody: `{"context": {"domain": "test-domain", "location": "test-location"}}`, + wantStatus: "ACK", + wantErr: false, + }, + { + name: "Empty context", + requestBody: `{"context": {}}`, + wantStatus: "ACK", + wantErr: false, + }, + { + name: "Invalid JSON", + requestBody: `{invalid json}`, + wantErr: true, + }, + { + name: "Complex nested context", + requestBody: `{"context": {"domain": "test-domain", "nested": {"key1": "value1", "key2": 123, "array": [1,2,3]}}}`, + wantStatus: "ACK", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := Ack(ctx, []byte(tt.requestBody)) + + if (err != nil) != tt.wantErr { + t.Errorf("Ack() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr && err != nil { + return + } + + var becknResp BecknResponse + if err := json.Unmarshal(resp, &becknResp); err != nil { + t.Errorf("Failed to unmarshal response: %v", err) + return + } + + if becknResp.Message.Ack.Status != tt.wantStatus { + t.Errorf("Ack() status = %v, want %v", becknResp.Message.Ack.Status, tt.wantStatus) + } + + if becknResp.Message.Error != nil { + t.Errorf("Ack() should not have error, got %v", becknResp.Message.Error) + } + + var origReq BecknRequest + if err := json.Unmarshal([]byte(tt.requestBody), &origReq); err == nil { + if !compareContexts(becknResp.Context, origReq.Context) { + t.Errorf("Ack() context not preserved, got = %v, want %v", becknResp.Context, origReq.Context) + } + } + }) + } +} + +func TestHandleClientFailure(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + errorType ErrorType + requestBody string + wantErrCode string + wantErrMsg string + wantErr bool + }{ + { + name: "Schema validation error", + errorType: SchemaValidationErrorType, + requestBody: `{"context": {"domain": "test-domain", "location": "test-location"}}`, + wantErrCode: "400", + wantErrMsg: "Schema validation failed", + wantErr: false, + }, + { + name: "Invalid request error", + errorType: InvalidRequestErrorType, + requestBody: `{"context": {"domain": "test-domain"}}`, + wantErrCode: "401", + wantErrMsg: "Invalid request format", + wantErr: false, + }, + { + name: "Unknown error type", + errorType: "UNKNOWN_ERROR", + requestBody: `{"context": {"domain": "test-domain"}}`, + wantErrCode: "500", + wantErrMsg: "Internal server error", + wantErr: false, + }, + { + name: "Invalid JSON", + errorType: SchemaValidationErrorType, + requestBody: `{invalid json}`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := HandleClientFailure(ctx, tt.errorType, []byte(tt.requestBody)) + + if (err != nil) != tt.wantErr { + t.Errorf("HandleClientFailure() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr && err != nil { + return + } + + var failureResp ClientFailureBecknResponse + if err := json.Unmarshal(resp, &failureResp); err != nil { + t.Errorf("Failed to unmarshal response: %v", err) + return + } + + if failureResp.Error.Code != tt.wantErrCode { + t.Errorf("HandleClientFailure() error code = %v, want %v", failureResp.Error.Code, tt.wantErrCode) + } + + if failureResp.Error.Message != tt.wantErrMsg { + t.Errorf("HandleClientFailure() error message = %v, want %v", failureResp.Error.Message, tt.wantErrMsg) + } + + var origReq BecknRequest + if err := json.Unmarshal([]byte(tt.requestBody), &origReq); err == nil { + if !compareContexts(failureResp.Context, origReq.Context) { + t.Errorf("HandleClientFailure() context not preserved, got = %v, want %v", failureResp.Context, origReq.Context) + } + } + }) + } +} + +func TestErrorMap(t *testing.T) { + + expectedTypes := []ErrorType{ + SchemaValidationErrorType, + InvalidRequestErrorType, + } + + for _, tp := range expectedTypes { + if _, exists := errorMap[tp]; !exists { + t.Errorf("ErrorType %v not found in errorMap", tp) + } + } + + if DefaultError.Code != "500" || DefaultError.Message != "Internal server error" { + t.Errorf("DefaultError not set correctly, got code=%v, message=%v", DefaultError.Code, DefaultError.Message) + } +} + +func compareContexts(c1, c2 map[string]interface{}) bool { + + if c1 == nil && c2 == nil { + return true + } + + if c1 == nil && len(c2) == 0 || c2 == nil && len(c1) == 0 { + return true + } + + return reflect.DeepEqual(c1, c2) +}