masih perubahan

This commit is contained in:
2025-09-17 05:30:04 +07:00
parent 87b69ddb29
commit 1d053646a9
9 changed files with 149 additions and 936 deletions

View File

@@ -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
```

View File

@@ -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])
}
}
})
}
}

View File

@@ -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")

View File

@@ -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

View File

@@ -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")
}
}

View File

@@ -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",
})
}

View File

@@ -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")
}

View File

@@ -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