diff --git a/README.md b/README.md index 30920272..31f12b10 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,11 @@ api-service/ │ ├── routes/ # 🛣️ API routing (Presentation) │ ├── services/ # 💼 Business logic (Application) │ └── repository/ # 💾 Data access (Infrastructure) +├── 📁 pkg/ # 💼 Package +│ ├── logger/ # 🎯 General generators +│ └── data/ # Hasil Log yang tersimpan +│ ├── utils/ # 🏥 BPJS specific tools +│ └── validator/ # 🩺 SATUSEHAT tools ├── 📁 tools/ # 🔧 Development tools │ ├── general/ # 🎯 General generators │ ├── bpjs/ # 🏥 BPJS specific tools @@ -114,6 +119,10 @@ make migrate-up go run cmd/api/main.go ``` +### Update Swagger +```bash +swag init -g cmd/api/main.go --parseDependency --parseInternal # Alternative +swag init -g cmd/api/main.go -o docs/ ### 3️⃣ Verify Installation @@ -256,16 +265,16 @@ go run tools/general/generate-handler.go orders get post put delete dynamic sear ```bash # Single service -go run tools/bpjs/generate-bpjs-handler.go reference/peserta get +go run tools/bpjs/generate-bpjs-handler.go tools/bpjs/reference/peserta get # Semua service dari config -go run tools/bpjs/generate-handler.go services-config-bpjs.yaml +go run tools/bpjs/generate-handler.go tools/bpjs/services-config-bpjs.yaml ``` **🩺 Generate SATUSEHAT Handler** ```bash -go run tools/satusehat/generate-satusehat-handler.go services-config-satusehat.yaml patient +go run tools/satusehat/generate-satusehat-handler.go tools/satusehat/services-config-satusehat.yaml patient ``` diff --git a/cmd/test_dynamic_logging/main.go b/cmd/logging/main.go similarity index 100% rename from cmd/test_dynamic_logging/main.go rename to cmd/logging/main.go diff --git a/dynamic_filter_test.go b/dynamic_filter_test.go deleted file mode 100644 index d229711d..00000000 --- a/dynamic_filter_test.go +++ /dev/null @@ -1,610 +0,0 @@ -package utils - -import ( - "testing" -) - -func TestQueryBuilder_BuildQuery(t *testing.T) { - builder := NewQueryBuilder("test_table"). - SetColumnMapping(map[string]string{ - "name": "full_name", - }). - SetAllowedColumns([]string{"id", "name", "age", "status"}) - - tests := []struct { - name string - query DynamicQuery - expected string - argsLen int - }{ - { - name: "simple equality filter", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "name", - Operator: OpEqual, - Value: "John", - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"full_name\" = $1", - argsLen: 1, - }, - { - name: "IN operator with multiple values", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "status", - Operator: OpIn, - Value: []interface{}{"active", "pending"}, - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"status\" IN ($1, $2)", - argsLen: 2, - }, - { - name: "BETWEEN operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "age", - Operator: OpBetween, - Value: []interface{}{"18", "65"}, - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"age\" BETWEEN $1 AND $2", - argsLen: 2, - }, - { - name: "NOT BETWEEN operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "age", - Operator: OpNotBetween, - Value: []interface{}{"18", "65"}, - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"age\" NOT BETWEEN $1 AND $2", - argsLen: 2, - }, - { - name: "LIKE operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "name", - Operator: OpLike, - Value: "John%", - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"full_name\" LIKE $1", - argsLen: 1, - }, - { - name: "ILIKE operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "name", - Operator: OpILike, - Value: "john%", - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"full_name\" ILIKE $1", - argsLen: 1, - }, - { - name: "greater than operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "age", - Operator: OpGreaterThan, - Value: "30", - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"age\" > $1", - argsLen: 1, - }, - { - name: "less than or equal operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "age", - Operator: OpLessThanEqual, - Value: "65", - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"age\" <= $1", - argsLen: 1, - }, - { - name: "NOT IN operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "status", - Operator: OpNotIn, - Value: []interface{}{"deleted", "inactive"}, - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"status\" NOT IN ($1, $2)", - argsLen: 2, - }, - { - name: "multiple filters with AND", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{ - { - Column: "status", - Operator: OpEqual, - Value: "active", - }, - { - Column: "age", - Operator: OpGreaterThan, - Value: "18", - }, - }, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"status\" = $1 AND \"age\" > $2", - argsLen: 2, - }, - { - name: "NULL check", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "name", - Operator: OpNull, - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"full_name\" IS NULL", - argsLen: 0, - }, - { - name: "NOT NULL check", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "name", - Operator: OpNotNull, - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"full_name\" IS NOT NULL", - argsLen: 0, - }, - { - name: "CONTAINS operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "name", - Operator: OpContains, - Value: "John", - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"full_name\" ILIKE $1", - argsLen: 1, - }, - { - name: "STARTS WITH operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "name", - Operator: OpStartsWith, - Value: "John", - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"full_name\" ILIKE $1", - argsLen: 1, - }, - { - name: "ENDS WITH operator", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "name", - Operator: OpEndsWith, - Value: "son", - }}, - }}, - }, - expected: "SELECT * FROM test_table WHERE \"full_name\" ILIKE $1", - argsLen: 1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sql, args, err := builder.BuildQuery(tt.query) - if err != nil { - t.Errorf("BuildQuery() error = %v", err) - return - } - - if sql != tt.expected { - t.Errorf("BuildQuery() sql = %v, expected %v", sql, tt.expected) - } - - if len(args) != tt.argsLen { - t.Errorf("BuildQuery() args length = %v, expected %v", len(args), tt.argsLen) - } - }) - } -} - -func TestQueryBuilder_BuildCountQuery(t *testing.T) { - builder := NewQueryBuilder("test_table"). - SetColumnMapping(map[string]string{ - "name": "full_name", - }). - SetAllowedColumns([]string{"id", "name", "age", "status"}) - - query := DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "status", - Operator: OpEqual, - Value: "active", - }}, - }}, - } - - sql, args, err := builder.BuildCountQuery(query) - if err != nil { - t.Errorf("BuildCountQuery() error = %v", err) - return - } - - expected := "SELECT COUNT(*) FROM test_table WHERE \"status\" = $1" - if sql != expected { - t.Errorf("BuildCountQuery() sql = %v, expected %v", sql, expected) - } - - if len(args) != 1 { - t.Errorf("BuildCountQuery() args length = %v, expected 1", len(args)) - } -} - -func TestQueryBuilder_WithSorting(t *testing.T) { - builder := NewQueryBuilder("test_table"). - SetColumnMapping(map[string]string{ - "name": "full_name", - }). - SetAllowedColumns([]string{"id", "name", "age", "status"}) - - query := DynamicQuery{ - Sort: []SortField{ - {Column: "name", Order: "ASC"}, - {Column: "age", Order: "DESC"}, - }, - } - - sql, _, err := builder.BuildQuery(query) - if err != nil { - t.Errorf("BuildQuery() error = %v", err) - return - } - - expected := "SELECT * FROM test_table ORDER BY \"full_name\" ASC, \"age\" DESC" - if sql != expected { - t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected) - } -} - -func TestQueryBuilder_WithPagination(t *testing.T) { - builder := NewQueryBuilder("test_table") - - query := DynamicQuery{ - Limit: 10, - Offset: 20, - } - - sql, args, err := builder.BuildQuery(query) - if err != nil { - t.Errorf("BuildQuery() error = %v", err) - return - } - - expected := "SELECT * FROM test_table LIMIT $1 OFFSET $2" - if sql != expected { - t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected) - } - - if len(args) != 2 { - t.Errorf("BuildQuery() args length = %v, expected 2", len(args)) - } - - if args[0] != 10 || args[1] != 20 { - t.Errorf("BuildQuery() args = %v, expected [10, 20]", args) - } -} - -func TestQueryBuilder_WithFields(t *testing.T) { - builder := NewQueryBuilder("test_table"). - SetAllowedColumns([]string{"id", "name", "age"}) - - query := DynamicQuery{ - Fields: []string{"id", "name"}, - } - - sql, _, err := builder.BuildQuery(query) - if err != nil { - t.Errorf("BuildQuery() error = %v", err) - return - } - - expected := "SELECT \"id\", \"name\" FROM test_table" - if sql != expected { - t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected) - } -} - -func TestQueryBuilder_SecurityChecks(t *testing.T) { - builder := NewQueryBuilder("test_table"). - SetAllowedColumns([]string{"id", "name"}) - - query := DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "password", // Not in allowed columns - Operator: OpEqual, - Value: "secret", - }}, - }}, - } - - sql, args, err := builder.BuildQuery(query) - if err != nil { - t.Errorf("BuildQuery() error = %v", err) - return - } - - // Should not include the password filter - expected := "SELECT * FROM test_table" - if sql != expected { - t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected) - } - - if len(args) != 0 { - t.Errorf("BuildQuery() args length = %v, expected 0", len(args)) - } -} - -func TestQueryBuilder_GroupByAndHaving(t *testing.T) { - builder := NewQueryBuilder("test_table"). - SetAllowedColumns([]string{"status", "count"}) - - query := DynamicQuery{ - Fields: []string{"status", "COUNT(*) as count"}, - GroupBy: []string{"status"}, - Having: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "count", - Operator: OpGreaterThan, - Value: "5", - }}, - }}, - } - - sql, args, err := builder.BuildQuery(query) - if err != nil { - t.Errorf("BuildQuery() error = %v", err) - return - } - - expected := "SELECT \"status\", COUNT(*) as count FROM test_table GROUP BY \"status\" HAVING \"count\" > $1" - if sql != expected { - t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected) - } - - if len(args) != 1 { - t.Errorf("BuildQuery() args length = %v, expected 1", len(args)) - } -} - -func TestQueryBuilder_ComplexQuery(t *testing.T) { - builder := NewQueryBuilder("test_table"). - SetColumnMapping(map[string]string{ - "name": "full_name", - }). - SetAllowedColumns([]string{"id", "name", "age", "status", "created_at"}) - - query := DynamicQuery{ - Fields: []string{"id", "name", "age"}, - Filters: []FilterGroup{{ - Filters: []DynamicFilter{ - { - Column: "status", - Operator: OpIn, - Value: []string{"active", "pending"}, - }, - { - Column: "age", - Operator: OpBetween, - Value: []interface{}{"18", "65"}, - }, - }, - }}, - Sort: []SortField{ - {Column: "name", Order: "ASC"}, - {Column: "created_at", Order: "DESC"}, - }, - Limit: 20, - Offset: 10, - } - - sql, args, err := builder.BuildQuery(query) - if err != nil { - t.Errorf("BuildQuery() error = %v", err) - return - } - - expected := "SELECT \"id\", \"full_name\", \"age\" FROM test_table WHERE \"status\" IN ($1, $2) AND \"age\" BETWEEN $3 AND $4 ORDER BY \"full_name\" ASC, \"created_at\" DESC LIMIT $5 OFFSET $6" - if sql != expected { - t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected) - } - - if len(args) != 6 { - t.Errorf("BuildQuery() args length = %v, expected 6", len(args)) - } -} - -func TestQueryBuilder_ErrorCases(t *testing.T) { - builder := NewQueryBuilder("test_table") - - tests := []struct { - name string - query DynamicQuery - }{ - { - name: "invalid between operator - not enough values", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "age", - Operator: OpBetween, - Value: []interface{}{"18"}, - }}, - }}, - }, - }, - { - name: "invalid not between operator - not enough values", - query: DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "age", - Operator: OpNotBetween, - Value: []interface{}{"18"}, - }}, - }}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, _, err := builder.BuildQuery(tt.query) - if err == nil { - t.Errorf("BuildQuery() expected error but got none") - } - }) - } -} - -func TestQueryBuilder_UnsupportedOperator(t *testing.T) { - builder := NewQueryBuilder("test_table") - - query := DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{{ - Column: "name", - Operator: FilterOperator("unsupported"), - Value: "test", - }}, - }}, - } - - _, _, err := builder.BuildQuery(query) - if err == nil { - t.Errorf("BuildQuery() expected error for unsupported operator") - } -} - -func TestQueryBuilder_EmptyFilters(t *testing.T) { - builder := NewQueryBuilder("test_table") - - query := DynamicQuery{ - Filters: []FilterGroup{{ - Filters: []DynamicFilter{}, - }}, - } - - sql, args, err := builder.BuildQuery(query) - if err != nil { - t.Errorf("BuildQuery() error = %v", err) - return - } - - expected := "SELECT * FROM test_table" - if sql != expected { - t.Errorf("BuildQuery() sql = %v, expected %v", sql, expected) - } - - if len(args) != 0 { - t.Errorf("BuildQuery() args length = %v, expected 0", len(args)) - } -} - -func TestQueryBuilder_ParseArrayValue(t *testing.T) { - builder := NewQueryBuilder("test_table") - - tests := []struct { - name string - input interface{} - expected []interface{} - }{ - { - name: "nil value", - input: nil, - expected: nil, - }, - { - name: "string value", - input: "test", - expected: []interface{}{"test"}, - }, - { - name: "comma separated string", - input: "a,b,c", - expected: []interface{}{"a", "b", "c"}, - }, - { - name: "slice value", - input: []interface{}{"a", "b", "c"}, - expected: []interface{}{"a", "b", "c"}, - }, - { - name: "integer value", - input: 42, - expected: []interface{}{42}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := builder.parseArrayValue(tt.input) - if len(result) != len(tt.expected) { - t.Errorf("parseArrayValue() length = %v, expected %v", len(result), len(tt.expected)) - return - } - - for i, v := range result { - if v != tt.expected[i] { - t.Errorf("parseArrayValue()[%d] = %v, expected %v", i, v, tt.expected[i]) - } - } - }) - } -} diff --git a/internal/config/config.go b/internal/config/config.go index 0411e77c..f34deb46 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,6 +91,26 @@ type SatuSehatConfig struct { } // SetHeader generates required headers for BPJS VClaim API +// func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) { +// timenow := time.Now().UTC() +// t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z") +// if err != nil { +// log.Fatal(err) +// } + +// tstamp := timenow.Unix() - t.Unix() +// secret := []byte(cfg.SecretKey) +// message := []byte(cfg.ConsID + "&" + fmt.Sprint(tstamp)) +// hash := hmac.New(sha256.New, secret) +// hash.Write(message) + +// // to lowercase hexits +// hex.EncodeToString(hash.Sum(nil)) +// // to base64 +// xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + +// return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature +// } func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) { timenow := time.Now().UTC() t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z") diff --git a/internal/services/bpjs/vclaimBridge.go b/internal/services/bpjs/vclaimBridge.go index 90f1f38d..ba484728 100644 --- a/internal/services/bpjs/vclaimBridge.go +++ b/internal/services/bpjs/vclaimBridge.go @@ -18,8 +18,6 @@ import ( "github.com/rs/zerolog/log" ) -// cleanResponse removes invalid characters and BOM from the response string - // VClaimService interface for VClaim operations type VClaimService interface { Get(ctx context.Context, endpoint string, result interface{}) error @@ -101,7 +99,7 @@ func (s *Service) SetHTTPClient(client *http.Client) { } // prepareRequest prepares HTTP request with required headers -func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) { +func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, string, string, string, string, error) { fullURL := s.config.BaseURL + endpoint log.Info(). @@ -117,11 +115,11 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b Str("method", method). Str("endpoint", endpoint). Msg("Failed to create HTTP request") - return nil, fmt.Errorf("failed to create request: %w", err) + return nil, "", "", "", "", fmt.Errorf("failed to create request: %w", err) } // Set headers using the SetHeader method - consID, _, userKey, tstamp, xSignature := s.config.SetHeader() + consID, secretKey, userKey, tstamp, xSignature := s.config.SetHeader() req.Header.Set("Content-Type", "application/json") req.Header.Set("X-cons-id", consID) @@ -137,11 +135,11 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b Str("user_key", userKey). Msg("Request headers set") - return req, nil + return req, consID, secretKey, tstamp, xSignature, nil } // processResponse processes response from VClaim API -func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) { +func (s *Service) processResponse(res *http.Response, consID, secretKey, tstamp string) (*ResponDTOVclaim, error) { defer res.Body.Close() body, err := io.ReadAll(res.Body) @@ -182,8 +180,7 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) return finalResp, nil } - // Decrypt response - consID, secretKey, _, tstamp, _ := s.config.SetHeader() + // Decrypt response using the same timestamp from the request decryptionKey := consID + secretKey + tstamp log.Debug(). @@ -192,17 +189,6 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) Int("key_length", len(decryptionKey)). Msg("Decryption key components") - // // Decrypt response - // consID, secretKey, userKey, tstamp, _ := s.config.SetHeader() - // decryptionKey := GenerateBPJSKey(consID, tstamp, secretKey) // Menggunakan fungsi baru - // log.Debug(). - // Str("consID", consID). - // Str("tstamp", tstamp). - // Str("userKey", userKey). - // Str("secretKey", secretKey). - // Int("key_length", len(decryptionKey)). - // Msg("Decryption key components") - respDecrypt, err := ResponseVclaim(respMentah.Response, decryptionKey) if err != nil { log.Error().Err(err).Msg("Failed to decrypt response") @@ -271,7 +257,7 @@ func (s *Service) Put(ctx context.Context, endpoint string, payload interface{}, } } - req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf) + req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf) if err != nil { return err } @@ -281,7 +267,7 @@ func (s *Service) Put(ctx context.Context, endpoint string, payload interface{}, return fmt.Errorf("failed to execute PUT request: %w", err) } - resp, err := s.processResponse(res) + resp, err := s.processResponse(res, consID, secretKey, tstamp) if err != nil { return err } @@ -291,7 +277,7 @@ func (s *Service) Put(ctx context.Context, endpoint string, payload interface{}, // Delete performs HTTP DELETE request func (s *Service) Delete(ctx context.Context, endpoint string, result interface{}) error { - req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil) + req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil) if err != nil { return err } @@ -301,7 +287,7 @@ func (s *Service) Delete(ctx context.Context, endpoint string, result interface{ return fmt.Errorf("failed to execute DELETE request: %w", err) } - resp, err := s.processResponse(res) + resp, err := s.processResponse(res, consID, secretKey, tstamp) if err != nil { return err } @@ -318,7 +304,7 @@ func (s *Service) Patch(ctx context.Context, endpoint string, payload interface{ } } - req, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf) + req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf) if err != nil { return err } @@ -328,7 +314,7 @@ func (s *Service) Patch(ctx context.Context, endpoint string, payload interface{ return fmt.Errorf("failed to execute PATCH request: %w", err) } - resp, err := s.processResponse(res) + resp, err := s.processResponse(res, consID, secretKey, tstamp) if err != nil { return err } @@ -338,7 +324,7 @@ func (s *Service) Patch(ctx context.Context, endpoint string, payload interface{ // GetRawResponse returns raw response without mapping func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) { - req, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil) + req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } @@ -348,7 +334,7 @@ func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponD return nil, fmt.Errorf("failed to execute GET request: %w", err) } - return s.processResponse(res) + return s.processResponse(res, consID, secretKey, tstamp) } // PostRawResponse returns raw response without mapping @@ -360,7 +346,7 @@ func (s *Service) PostRawResponse(ctx context.Context, endpoint string, payload } } - req, err := s.prepareRequest(ctx, http.MethodPost, endpoint, &buf) + req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPost, endpoint, &buf) if err != nil { return nil, err } @@ -370,7 +356,7 @@ func (s *Service) PostRawResponse(ctx context.Context, endpoint string, payload return nil, fmt.Errorf("failed to execute POST request: %w", err) } - return s.processResponse(res) + return s.processResponse(res, consID, secretKey, tstamp) } // PatchRawResponse returns raw response without mapping @@ -382,7 +368,7 @@ func (s *Service) PatchRawResponse(ctx context.Context, endpoint string, payload } } - req, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf) + req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf) if err != nil { return nil, err } @@ -392,7 +378,7 @@ func (s *Service) PatchRawResponse(ctx context.Context, endpoint string, payload return nil, fmt.Errorf("failed to execute PATCH request: %w", err) } - return s.processResponse(res) + return s.processResponse(res, consID, secretKey, tstamp) } // PutRawResponse returns raw response without mapping @@ -404,7 +390,7 @@ func (s *Service) PutRawResponse(ctx context.Context, endpoint string, payload i } } - req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf) + req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf) if err != nil { return nil, err } @@ -414,12 +400,12 @@ func (s *Service) PutRawResponse(ctx context.Context, endpoint string, payload i return nil, fmt.Errorf("failed to execute PUT request: %w", err) } - return s.processResponse(res) + return s.processResponse(res, consID, secretKey, tstamp) } // DeleteRawResponse returns raw response without mapping func (s *Service) DeleteRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) { - req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil) + req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil) if err != nil { return nil, err } @@ -429,7 +415,7 @@ func (s *Service) DeleteRawResponse(ctx context.Context, endpoint string) (*Resp return nil, fmt.Errorf("failed to execute DELETE request: %w", err) } - return s.processResponse(res) + return s.processResponse(res, consID, secretKey, tstamp) } // mapToResult maps the final response to the result interface @@ -498,6 +484,7 @@ func PostRequest(endpoint string, cfg interface{}, data interface{}) interface{} return resp } + func cleanResponse(s string) string { // Remove UTF-8 BOM dan variasi BOM lainnya s = strings.TrimPrefix(s, "\xef\xbb\xbf") // UTF-8 BOM diff --git a/pkg/logger/dynamic_logging_test.go b/pkg/logger/dynamic_logging_test.go deleted file mode 100644 index 5a0c78df..00000000 --- a/pkg/logger/dynamic_logging_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package logger - -import ( - "os" - "testing" - "time" -) - -func TestDynamicLogging(t *testing.T) { - // Pastikan direktori data ada - os.RemoveAll("pkg/logger/data") - - t.Run("TestSaveLogText", testSaveLogText) - t.Run("TestSaveLogJSON", testSaveLogJSON) - t.Run("TestSaveLogToDatabase", testSaveLogToDatabase) -} - -func testSaveLogText(t *testing.T) { - logger := New("test-service", INFO, false) - - entry := LogEntry{ - Timestamp: time.Now().Format(time.RFC3339), - Level: "INFO", - Service: "test-service", - Message: "Test log message", - File: "test.go", - Line: 10, - Fields: map[string]interface{}{ - "test_field": "test_value", - "number": 42, - }, - } - - err := logger.SaveLogText(entry) - if err != nil { - t.Errorf("SaveLogText failed: %v", err) - } - - // Verifikasi file dibuat - if _, err := os.Stat("pkg/logger/data/logs.txt"); os.IsNotExist(err) { - t.Error("Text log file was not created") - } -} - -func testSaveLogJSON(t *testing.T) { - logger := New("test-service", INFO, false) - - entry := LogEntry{ - Timestamp: time.Now().Format(time.RFC3339), - Level: "INFO", - Service: "test-service", - Message: "Test JSON log message", - File: "test.go", - Line: 20, - Fields: map[string]interface{}{ - "json_field": "json_value", - "count": 100, - }, - } - - err := logger.SaveLogJSON(entry) - if err != nil { - t.Errorf("SaveLogJSON failed: %v", err) - } - - // Verifikasi file dibuat - if _, err := os.Stat("pkg/logger/data/logs.json"); os.IsNotExist(err) { - t.Error("JSON log file was not created") - } -} - -func testSaveLogToDatabase(t *testing.T) { - logger := New("test-service", INFO, false) - - entry := LogEntry{ - Timestamp: time.Now().Format(time.RFC3339), - Level: "INFO", - Service: "test-service", - Message: "Test database log message", - File: "test.go", - Line: 30, - } - - err := logger.SaveLogToDatabase(entry) - if err != nil { - t.Errorf("SaveLogToDatabase failed: %v", err) - } - - // Verifikasi file dibuat (placeholder untuk database) - if _, err := os.Stat("pkg/logger/data/database_logs.txt"); os.IsNotExist(err) { - t.Error("Database log file was not created") - } -} diff --git a/pkg/logger/example_dynamic_logging.go b/pkg/logger/example_dynamic_logging.go deleted file mode 100644 index ba2f7196..00000000 --- a/pkg/logger/example_dynamic_logging.go +++ /dev/null @@ -1,105 +0,0 @@ -package logger - -import ( - "fmt" - "time" -) - -// ExampleDynamicLogging menunjukkan cara menggunakan fungsi penyimpanan log dinamis -func ExampleDynamicLogging() { - // Buat logger instance - logger := New("test-service", DEBUG, false) - - // Contoh 1: Log biasa dengan penyimpanan otomatis - fmt.Println("=== Contoh 1: Log biasa dengan penyimpanan otomatis ===") - logger.LogAndSave(INFO, "Aplikasi dimulai", map[string]interface{}{ - "version": "1.0.0", - "mode": "development", - }) - - // Contoh 2: Log dengan request ID - fmt.Println("\n=== Contoh 2: Log dengan request ID ===") - reqLogger := logger.WithRequestID("req-123456") - reqLogger.LogAndSave(INFO, "Request diproses", map[string]interface{}{ - "endpoint": "/api/v1/users", - "method": "GET", - "user_id": 1001, - }) - - // Contoh 3: Log error - fmt.Println("\n=== Contoh 3: Log error ===") - logger.LogAndSave(ERROR, "Database connection failed", map[string]interface{}{ - "error": "connection timeout", - "timeout": "30s", - "host": "localhost:5432", - }) - - // Contoh 4: Manual save ke berbagai format - fmt.Println("\n=== Contoh 4: Manual save ke berbagai format ===") - manualEntry := LogEntry{ - Timestamp: time.Now().Format(time.RFC3339), - Level: "INFO", - Service: "manual-service", - Message: "Manual log entry", - RequestID: "manual-req-001", - File: "example.go", - Line: 42, - Fields: map[string]interface{}{ - "custom_field": "custom_value", - "number": 42, - }, - } - - // Simpan manual ke berbagai format - if err := SaveLogText(manualEntry); err != nil { - fmt.Printf("Error saving text log: %v\n", err) - } - - if err := SaveLogJSON(manualEntry); err != nil { - fmt.Printf("Error saving JSON log: %v\n", err) - } - - if err := SaveLogToDatabase(manualEntry); err != nil { - fmt.Printf("Error saving to database log: %v\n", err) - } - - // Contoh 5: Log dengan durasi - fmt.Println("\n=== Contoh 5: Log dengan durasi ===") - start := time.Now() - time.Sleep(100 * time.Millisecond) // Simulasi proses - duration := time.Since(start) - - logger.LogAndSave(INFO, "Process completed", map[string]interface{}{ - "operation": "data_processing", - "duration": duration.String(), - "items": 150, - }) - - fmt.Println("\n=== Semua log telah disimpan dalam berbagai format ===") - fmt.Println("1. Format teks dengan pemisah |: pkg/logger/data/logs.txt") - fmt.Println("2. Format JSON: pkg/logger/data/logs.json") - fmt.Println("3. Format database (placeholder): pkg/logger/data/database_logs.txt") -} - -// ExampleMiddlewareLogging menunjukkan penggunaan dalam middleware -func ExampleMiddlewareLogging() { - fmt.Println("\n=== Contoh Penggunaan dalam Middleware ===") - - middlewareLogger := New("middleware-service", INFO, false) - - // Simulasi request processing - middlewareLogger.LogAndSave(INFO, "Request received", map[string]interface{}{ - "method": "POST", - "path": "/api/v1/auth/login", - "client_ip": "192.168.1.100", - "user_agent": "Mozilla/5.0", - "content_type": "application/json", - }) - - // Simulasi response - middlewareLogger.LogAndSave(INFO, "Response sent", map[string]interface{}{ - "status_code": 200, - "duration": "150ms", - "response_size": "2.5KB", - }) -} diff --git a/pkg/logger/example_test.go b/pkg/logger/example_test.go deleted file mode 100644 index 9c2718fb..00000000 --- a/pkg/logger/example_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package logger - -import ( - "testing" - "time" -) - -func TestLoggerExamples(t *testing.T) { - // Example 1: Basic logging - Info("Application starting up") - Debug("Debug information", map[string]interface{}{"config_loaded": true}) - - // Example 2: Service-specific logging - authLogger := AuthServiceLogger() - authLogger.Info("User authentication successful", map[string]interface{}{ - "user_id": "12345", - "method": "oauth2", - }) - - // Example 3: Error logging with context - Error("Database connection failed", map[string]interface{}{ - "error": "connection timeout", - "retry_count": 3, - "max_retries": 5, - "service": "database-service", - }) - - // Example 4: Performance timing - start := time.Now() - time.Sleep(10 * time.Millisecond) // Simulate work - globalLogger.LogDuration(start, "Database query completed", map[string]interface{}{ - "query": "SELECT * FROM users", - "rows": 150, - "database": "postgres", - }) - - // Example 5: JSON format logging - jsonLogger := New("test-service", DEBUG, true) - jsonLogger.Info("JSON formatted log", map[string]interface{}{ - "user": map[string]interface{}{ - "id": "user-123", - "name": "John Doe", - "email": "john@example.com", - }, - "request": map[string]interface{}{ - "method": "GET", - "path": "/api/v1/users", - }, - }) - - t.Log("Logger examples executed successfully") -} - -func TestLoggerLevels(t *testing.T) { - // Test different log levels - Debug("This is a debug message") - Info("This is an info message") - Warn("This is a warning message") - Error("This is an error message") - - t.Log("All log levels tested") -} - -func TestLoggerWithRequestContext(t *testing.T) { - // Simulate request context with IDs - logger := Default(). - WithRequestID("req-123456"). - WithCorrelationID("corr-789012") - - logger.Info("Request processing started", map[string]interface{}{ - "endpoint": "/api/v1/data", - "method": "POST", - "client_ip": "192.168.1.100", - }) - - t.Log("Request context logging tested") -} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 632d18fe..bcdd59e9 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "path/filepath" "runtime" "strings" "sync" @@ -47,6 +48,8 @@ type Logger struct { output *log.Logger mu sync.Mutex jsonFormat bool + + logDir string } // LogEntry represents a structured log entry @@ -64,12 +67,81 @@ type LogEntry struct { } // New creates a new logger instance -func New(serviceName string, level LogLevel, jsonFormat bool) *Logger { +func New(serviceName string, level LogLevel, jsonFormat bool, logDir ...string) *Logger { + // Tentukan direktori log berdasarkan prioritas: + // 1. Parameter logDir (jika disediakan) + // 2. Environment variable LOG_DIR (jika ada) + // 3. Default ke pkg/logger/data relatif terhadap root proyek + + var finalLogDir string + + // Cek apakah logDir disediakan sebagai parameter + if len(logDir) > 0 && logDir[0] != "" { + finalLogDir = logDir[0] + } else { + // Cek environment variable + if envLogDir := os.Getenv("LOG_DIR"); envLogDir != "" { + finalLogDir = envLogDir + } else { + // Default: dapatkan path relatif terhadap root proyek + // Dapatkan path executable + exePath, err := os.Executable() + if err != nil { + // Fallback ke current working directory jika gagal + finalLogDir = filepath.Join(".", "pkg", "logger", "data") + } else { + // Dapatkan direktori executable + exeDir := filepath.Dir(exePath) + + // Jika berjalan dengan go run, executable ada di temp directory + // Coba dapatkan path source code + if strings.Contains(exeDir, "go-build") || strings.Contains(exeDir, "tmp") { + // Gunakan runtime.Caller untuk mendapatkan path source + _, file, _, ok := runtime.Caller(0) + if ok { + // Dapatkan direktori source (2 level up dari pkg/logger) + sourceDir := filepath.Dir(file) + for i := 0; i < 3; i++ { // Naik 3 level ke root proyek + sourceDir = filepath.Dir(sourceDir) + } + finalLogDir = filepath.Join(sourceDir, "pkg", "logger", "data") + } else { + // Fallback + finalLogDir = filepath.Join(".", "pkg", "logger", "data") + } + } else { + // Untuk binary yang sudah dikompilasi, asumsikan struktur proyek + finalLogDir = filepath.Join(exeDir, "pkg", "logger", "data") + } + } + } + } + + // Konversi ke path absolut + absPath, err := filepath.Abs(finalLogDir) + if err == nil { + finalLogDir = absPath + } + + // Buat direktori jika belum ada + if err := os.MkdirAll(finalLogDir, 0755); err != nil { + // Fallback ke stdout jika gagal membuat direktori + fmt.Printf("Warning: Failed to create log directory %s: %v\n", finalLogDir, err) + return &Logger{ + serviceName: serviceName, + level: level, + output: log.New(os.Stdout, "", 0), + jsonFormat: jsonFormat, + logDir: "", // Kosongkan karena gagal + } + } + return &Logger{ serviceName: serviceName, level: level, output: log.New(os.Stdout, "", 0), jsonFormat: jsonFormat, + logDir: finalLogDir, } } @@ -95,6 +167,7 @@ func (l *Logger) WithService(serviceName string) *Logger { level: l.level, output: l.output, jsonFormat: l.jsonFormat, + logDir: l.logDir, } } @@ -186,6 +259,7 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger { level: l.level, output: l.output, jsonFormat: l.jsonFormat, + logDir: l.logDir, } } @@ -312,6 +386,7 @@ func (l *Logger) withField(key string, value interface{}) *Logger { level: l.level, output: l.output, jsonFormat: l.jsonFormat, + logDir: l.logDir, } } @@ -402,13 +477,15 @@ func (l *Logger) SaveLogText(entry LogEntry) error { logLine += "\n" // Buat direktori jika belum ada - dirPath := "pkg/logger/data" - if err := os.MkdirAll(dirPath, 0755); err != nil { + if err := os.MkdirAll(l.logDir, 0755); err != nil { return err } - // Tulis ke file - filePath := dirPath + "/logs.txt" + // Tulis ke file dengan mutex lock untuk concurrency safety + l.mu.Lock() + defer l.mu.Unlock() + + filePath := filepath.Join(l.logDir, "logs.txt") f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err @@ -429,13 +506,15 @@ func (l *Logger) SaveLogJSON(entry LogEntry) error { } // Buat direktori jika belum ada - dirPath := "pkg/logger/data" - if err := os.MkdirAll(dirPath, 0755); err != nil { + if err := os.MkdirAll(l.logDir, 0755); err != nil { return err } - // Tulis ke file - filePath := dirPath + "/logs.json" + // Tulis ke file dengan mutex lock for concurrency safety + l.mu.Lock() + defer l.mu.Unlock() + + filePath := filepath.Join(l.logDir, "logs.json") f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err @@ -458,12 +537,15 @@ func (l *Logger) SaveLogToDatabase(entry LogEntry) error { dbLogLine := fmt.Sprintf("DB_LOG: %s|%s|%s|%s\n", entry.Timestamp, entry.Level, entry.Service, entry.Message) - dirPath := "pkg/logger/data" - if err := os.MkdirAll(dirPath, 0755); err != nil { + if err := os.MkdirAll(l.logDir, 0755); err != nil { return err } - filePath := dirPath + "/database_logs.txt" + // Tulis ke file dengan mutex lock for concurrency safety + l.mu.Lock() + defer l.mu.Unlock() + + filePath := filepath.Join(l.logDir, "database_logs.txt") f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err