diff --git a/.github/workflows/beckn_ci_test.yml b/.github/workflows/beckn_ci_test.yml index fb6a4cf..e8d9ae6 100644 --- a/.github/workflows/beckn_ci_test.yml +++ b/.github/workflows/beckn_ci_test.yml @@ -1,64 +1,55 @@ name: CI/CD Test Pipeline - + on: pull_request: branches: - beckn-onix-v1.0-develop - + env: - APP_DIRECTORY: "shared/plugin" # Root directory to start searching from - + APP_DIRECTORY: "shared/plugin" + jobs: lint_and_test: runs-on: ubuntu-latest if: github.event_name == 'pull_request' - timeout-minutes: 10 # Increased timeout due to additional steps + timeout-minutes: 10 + outputs: + coverage_ok: ${{ steps.coverage_check.outputs.coverage_ok }} steps: - # 1. Checkout the code from the test branch (triggered by PR) - name: Checkout code uses: actions/checkout@v4 - - # 2. Set up Go environment + - name: Set up Go 1.24.0 uses: actions/setup-go@v4 with: go-version: '1.24.0' - - # 3. Install golangci-lint + - name: Install golangci-lint - run: | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - - # 4. Run golangci-lint on the entire repo, starting from the root directory + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + - name: 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 + run: golangci-lint run ./... + - name: Run unit tests with coverage run: | - # Create a directory to store coverage files mkdir -p $GITHUB_WORKSPACE/coverage_files - # Find all *_test.go files test_files=$(find ./ -type f -name '*_test.go') - # Check if there are any test files if [ -z "$test_files" ]; then - echo "No test cases found in the repository. Skipping test execution." + echo "No test cases found. Skipping." exit 0 fi - # Run tests and store coverage for each test directory for test_file in $test_files; do test_dir=$(dirname "$test_file") go_file="${test_file/_test.go/.go}" 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 || echo "Tests failed, but continuing." done - - # 6. Check coverage for each generated coverage file + - name: Check coverage for each test file + id: coverage_check run: | + echo "coverage_ok=true" >> $GITHUB_OUTPUT coverage_files=$(find $GITHUB_WORKSPACE/coverage_files -name "coverage_*.out") - # If no coverage files were generated, exit without failure if [ -z "$coverage_files" ]; then echo "No coverage files found. Skipping coverage check." exit 0 @@ -66,9 +57,29 @@ jobs: for coverage_file in $coverage_files; do echo "Checking coverage for $coverage_file" coverage=$(go tool cover -func=$coverage_file | grep total | awk '{print $3}' | sed 's/%//') - echo "Coverage for $coverage_file: $coverage%" + echo "Coverage: $coverage%" if (( $(echo "$coverage < 90" | bc -l) )); then - echo "Coverage for $coverage_file is below 90%. Failing the job." - exit 1 + echo "coverage_ok=false" >> $GITHUB_OUTPUT + break fi done + + require_exception_approval: + needs: lint_and_test + if: needs.lint_and_test.outputs.coverage_ok == 'false' + runs-on: ubuntu-latest + environment: + name: coverage-exception + url: https://your-coverage-dashboard.com # Optional + steps: + - name: Manual approval required + run: echo "Coverage < 90%. Approval required to continue." + + proceed_with_merge: + needs: [lint_and_test, require_exception_approval] + if: | + needs.lint_and_test.outputs.coverage_ok == 'true' || success() + runs-on: ubuntu-latest + steps: + - name: Proceed with merge + run: echo "Coverage requirement met or exception approved. Merge allowed." diff --git a/config/onix-bap/adapter.yaml b/config/onix-bap/adapter.yaml index a95c53f..dda19fc 100644 --- a/config/onix-bap/adapter.yaml +++ b/config/onix-bap/adapter.yaml @@ -7,6 +7,7 @@ log: - transaction_id - message_id - subscriber_id + - module_id http: port: 8080 timeout: diff --git a/config/onix-bpp/adapter.yaml b/config/onix-bpp/adapter.yaml index 8994980..aa3d242 100644 --- a/config/onix-bpp/adapter.yaml +++ b/config/onix-bpp/adapter.yaml @@ -7,6 +7,7 @@ log: - transaction_id - message_id - subscriber_id + - module_id http: port: 8080 timeout: diff --git a/config/onix/adapter.yaml b/config/onix/adapter.yaml index a626c4e..e3a785b 100644 --- a/config/onix/adapter.yaml +++ b/config/onix/adapter.yaml @@ -7,6 +7,7 @@ log: - transaction_id - message_id - subscriber_id + - module_id http: port: 8080 timeout: diff --git a/core/module/module.go b/core/module/module.go index 3b0fcef..4641fcb 100644 --- a/core/module/module.go +++ b/core/module/module.go @@ -7,6 +7,7 @@ import ( "github.com/beckn/beckn-onix/core/module/handler" "github.com/beckn/beckn-onix/pkg/log" + "github.com/beckn/beckn-onix/pkg/model" ) // Config represents the configuration for a module. @@ -44,6 +45,7 @@ func Register(ctx context.Context, mCfgs []Config, mux *http.ServeMux, mgr handl return fmt.Errorf("failed to add middleware: %w", err) } + h = moduleCtxMiddleware(c.Name, h) log.Debugf(ctx, "Registering handler %s, of type %s @ %s", c.Name, c.Handler.Type, c.Path) mux.Handle(c.Path, h) } @@ -71,3 +73,10 @@ func addMiddleware(ctx context.Context, mgr handler.PluginManager, handler http. log.Debugf(ctx, "Middleware chain setup completed") return handler, nil } + +func moduleCtxMiddleware(moduleName string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), model.ContextKeyModuleId, moduleName) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/core/module/module_test.go b/core/module/module_test.go index 8f9016c..6091fc1 100644 --- a/core/module/module_test.go +++ b/core/module/module_test.go @@ -4,9 +4,11 @@ import ( "context" "errors" "net/http" + "net/http/httptest" "testing" "github.com/beckn/beckn-onix/core/module/handler" + "github.com/beckn/beckn-onix/pkg/model" "github.com/beckn/beckn-onix/pkg/plugin" "github.com/beckn/beckn-onix/pkg/plugin/definition" ) @@ -97,6 +99,26 @@ func TestRegisterSuccess(t *testing.T) { if err != nil { t.Errorf("unexpected error: %v", err) } + + // Create a request and a response recorder + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + + // Create a handler that extracts context + var capturedModuleName any + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedModuleName = r.Context().Value(model.ContextKeyModuleId) + w.WriteHeader(http.StatusOK) + }) + + wrappedHandler := moduleCtxMiddleware("test-module", testHandler) + wrappedHandler.ServeHTTP(rec, req) + + // Now verify if module name exists in context + if capturedModuleName != "test-module" { + t.Errorf("expected module_id in context to be 'test-module', got %v", capturedModuleName) + } + } // TestRegisterFailure tests scenarios where the handler registration should fail. diff --git a/pkg/model/model.go b/pkg/model/model.go index a91e2a3..1193dee 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -16,6 +16,10 @@ type Subscriber struct { Domain string `json:"domain"` } +type ContextKey string + +const ContextKeyModuleId ContextKey = "module_id" + // Subscription represents subscription details of a network participant. type Subscription struct { Subscriber `json:",inline"` diff --git a/pkg/plugin/implementation/decrypter/decrypter.go b/pkg/plugin/implementation/decrypter/decrypter.go index f312f16..f03d438 100644 --- a/pkg/plugin/implementation/decrypter/decrypter.go +++ b/pkg/plugin/implementation/decrypter/decrypter.go @@ -9,6 +9,8 @@ import ( "fmt" "github.com/zenazn/pkcs7pad" + + "github.com/beckn/beckn-onix/pkg/model" ) // decrypter implements the Decrypter interface and handles the decryption process. @@ -24,18 +26,18 @@ func New(ctx context.Context) (*decrypter, func() error, error) { func (d *decrypter) Decrypt(ctx context.Context, encryptedData, privateKeyBase64, publicKeyBase64 string) (string, error) { privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64) if err != nil { - return "", fmt.Errorf("invalid private key: %w", err) + return "", model.NewBadReqErr(fmt.Errorf("invalid private key: %w", err)) } publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64) if err != nil { - return "", fmt.Errorf("invalid public key: %w", err) + return "", model.NewBadReqErr(fmt.Errorf("invalid public key: %w", err)) } // Decode the Base64 encoded encrypted data. messageByte, err := base64.StdEncoding.DecodeString(encryptedData) if err != nil { - return "", fmt.Errorf("failed to decode encrypted data: %w", err) + return "", model.NewBadReqErr(fmt.Errorf("failed to decode encrypted data: %w", err)) } aesCipher, err := createAESCipher(privateKeyBytes, publicKeyBytes) diff --git a/pkg/plugin/implementation/encrypter/encrypter.go b/pkg/plugin/implementation/encrypter/encrypter.go index f0a8663..301cf0a 100644 --- a/pkg/plugin/implementation/encrypter/encrypter.go +++ b/pkg/plugin/implementation/encrypter/encrypter.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "fmt" + "github.com/beckn/beckn-onix/pkg/model" "github.com/zenazn/pkcs7pad" ) @@ -23,12 +24,12 @@ func New(ctx context.Context) (*encrypter, func() error, error) { func (e *encrypter) Encrypt(ctx context.Context, data string, privateKeyBase64, publicKeyBase64 string) (string, error) { privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64) if err != nil { - return "", fmt.Errorf("invalid private key: %w", err) + return "", model.NewBadReqErr(fmt.Errorf("invalid private key: %w", err)) } publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64) if err != nil { - return "", fmt.Errorf("invalid public key: %w", err) + return "", model.NewBadReqErr(fmt.Errorf("invalid public key: %w", err)) } // Convert the input string to a byte slice. @@ -50,11 +51,11 @@ func createAESCipher(privateKey, publicKey []byte) (cipher.Block, error) { x25519Curve := ecdh.X25519() x25519PrivateKey, err := x25519Curve.NewPrivateKey(privateKey) if err != nil { - return nil, fmt.Errorf("failed to create private key: %w", err) + return nil, model.NewBadReqErr(fmt.Errorf("failed to create private key: %w", err)) } x25519PublicKey, err := x25519Curve.NewPublicKey(publicKey) if err != nil { - return nil, fmt.Errorf("failed to create public key: %w", err) + return nil, model.NewBadReqErr(fmt.Errorf("failed to create public key: %w", err)) } sharedSecret, err := x25519PrivateKey.ECDH(x25519PublicKey) if err != nil { diff --git a/pkg/plugin/implementation/schemavalidator/schemavalidator.go b/pkg/plugin/implementation/schemavalidator/schemavalidator.go index 715def7..bab2dba 100644 --- a/pkg/plugin/implementation/schemavalidator/schemavalidator.go +++ b/pkg/plugin/implementation/schemavalidator/schemavalidator.go @@ -3,6 +3,7 @@ package schemavalidator import ( "context" "encoding/json" + "errors" "fmt" "net/url" "os" @@ -10,6 +11,7 @@ import ( "path/filepath" "strings" + "github.com/beckn/beckn-onix/pkg/log" "github.com/beckn/beckn-onix/pkg/model" "github.com/santhosh-tekuri/jsonschema/v6" @@ -57,7 +59,7 @@ func (v *schemaValidator) Validate(ctx context.Context, url *url.URL, data []byt var payloadData payload err := json.Unmarshal(data, &payloadData) if err != nil { - return fmt.Errorf("failed to parse JSON payload: %v", err) + return model.NewBadReqErr(fmt.Errorf("failed to parse JSON payload: %v", err)) } // Extract domain, version, and endpoint from the payload and uri. @@ -66,8 +68,7 @@ func (v *schemaValidator) Validate(ctx context.Context, url *url.URL, data []byt version = fmt.Sprintf("v%s", version) endpoint := path.Base(url.String()) - // ToDo Add debug log here - fmt.Println("Handling request for endpoint:", endpoint) + log.Debugf(ctx, "Handling request for endpoint: %s", endpoint) domain := strings.ToLower(cxtDomain) domain = strings.ReplaceAll(domain, ":", "_") @@ -77,12 +78,12 @@ func (v *schemaValidator) Validate(ctx context.Context, url *url.URL, data []byt // Retrieve the schema from the cache. schema, exists := v.schemaCache[schemaFileName] if !exists { - return fmt.Errorf("schema not found for domain: %s", schemaFileName) + return model.NewBadReqErr(errors.New("schema not found for domain")) } var jsonData any if err := json.Unmarshal(data, &jsonData); err != nil { - return fmt.Errorf("failed to parse JSON data: %v", err) + return model.NewBadReqErr(fmt.Errorf("failed to parse JSON data: %v", err)) } err = schema.Validate(jsonData) if err != nil { @@ -104,7 +105,6 @@ func (v *schemaValidator) Validate(ctx context.Context, url *url.URL, data []byt // Return the array of schema validation errors return &model.SchemaValidationErr{Errors: schemaErrors} } - // Return a generic error for non-validation errors return fmt.Errorf("validation failed: %v", err) } @@ -112,9 +112,6 @@ func (v *schemaValidator) Validate(ctx context.Context, url *url.URL, data []byt return nil } -// ValidatorProvider provides instances of Validator. -type ValidatorProvider struct{} - // Initialise initialises the validator provider by compiling all the JSON schema files // from the specified directory and storing them in a cache indexed by their schema filenames. func (v *schemaValidator) initialise() error { diff --git a/pkg/plugin/implementation/signvalidator/signvalidator.go b/pkg/plugin/implementation/signvalidator/signvalidator.go index c381d40..612d857 100644 --- a/pkg/plugin/implementation/signvalidator/signvalidator.go +++ b/pkg/plugin/implementation/signvalidator/signvalidator.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/beckn/beckn-onix/pkg/model" "golang.org/x/crypto/blake2b" ) @@ -32,20 +33,17 @@ func New(ctx context.Context, config *Config) (*validator, func() error, error) func (v *validator) Validate(ctx context.Context, body []byte, header string, publicKeyBase64 string) error { createdTimestamp, expiredTimestamp, signature, err := parseAuthHeader(header) if err != nil { - // TODO: Return appropriate error code when Error Code Handling Module is ready - return fmt.Errorf("error parsing header: %w", err) + return model.NewSignValidationErr(fmt.Errorf("error parsing header: %w", err)) } signatureBytes, err := base64.StdEncoding.DecodeString(signature) if err != nil { - // TODO: Return appropriate error code when Error Code Handling Module is ready return fmt.Errorf("error decoding signature: %w", err) } currentTime := time.Now().Unix() if createdTimestamp > currentTime || currentTime > expiredTimestamp { - // TODO: Return appropriate error code when Error Code Handling Module is ready - return fmt.Errorf("signature is expired or not yet valid") + return model.NewSignValidationErr(fmt.Errorf("signature is expired or not yet valid")) } createdTime := time.Unix(createdTimestamp, 0) @@ -55,13 +53,11 @@ func (v *validator) Validate(ctx context.Context, body []byte, header string, pu decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKeyBase64) if err != nil { - // TODO: Return appropriate error code when Error Code Handling Module is ready - return fmt.Errorf("error decoding public key: %w", err) + return model.NewSignValidationErr(fmt.Errorf("error decoding public key: %w", err)) } if !ed25519.Verify(ed25519.PublicKey(decodedPublicKey), []byte(signingString), signatureBytes) { - // TODO: Return appropriate error code when Error Code Handling Module is ready - return fmt.Errorf("signature verification failed") + return model.NewSignValidationErr(fmt.Errorf("signature verification failed")) } return nil @@ -91,14 +87,13 @@ func parseAuthHeader(header string) (int64, int64, string, error) { expiredTimestamp, err := strconv.ParseInt(signatureMap["expires"], 10, 64) if err != nil { - // TODO: Return appropriate error code when Error Code Handling Module is ready - return 0, 0, "", fmt.Errorf("invalid expires timestamp: %w", err) + return 0, 0, "", model.NewSignValidationErr(fmt.Errorf("invalid expires timestamp: %w", err)) } signature := signatureMap["signature"] if signature == "" { // TODO: Return appropriate error code when Error Code Handling Module is ready - return 0, 0, "", fmt.Errorf("signature missing in header") + return 0, 0, "", model.NewSignValidationErr(fmt.Errorf("signature missing in header")) } return createdTimestamp, expiredTimestamp, signature, nil diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 4cb4a37..12d5909 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -131,7 +131,7 @@ func (m *Manager) Publisher(ctx context.Context, cfg *Config) (definition.Publis return nil, err } if closer != nil { - m.addCloser(func() { + m.closers = append(m.closers, func() { if err := closer(); err != nil { panic(err) } @@ -159,7 +159,7 @@ func (m *Manager) SchemaValidator(ctx context.Context, cfg *Config) (definition. return nil, err } if closer != nil { - m.addCloser(func() { + m.closers = append(m.closers, func() { if err := closer(); err != nil { panic(err) }