masih perubahan
This commit is contained in:
15
README.md
15
README.md
@@ -67,6 +67,11 @@ api-service/
|
|||||||
│ ├── routes/ # 🛣️ API routing (Presentation)
|
│ ├── routes/ # 🛣️ API routing (Presentation)
|
||||||
│ ├── services/ # 💼 Business logic (Application)
|
│ ├── services/ # 💼 Business logic (Application)
|
||||||
│ └── repository/ # 💾 Data access (Infrastructure)
|
│ └── repository/ # 💾 Data access (Infrastructure)
|
||||||
|
├── 📁 pkg/ # 💼 Package
|
||||||
|
│ ├── logger/ # 🎯 General generators
|
||||||
|
│ └── data/ # Hasil Log yang tersimpan
|
||||||
|
│ ├── utils/ # 🏥 BPJS specific tools
|
||||||
|
│ └── validator/ # 🩺 SATUSEHAT tools
|
||||||
├── 📁 tools/ # 🔧 Development tools
|
├── 📁 tools/ # 🔧 Development tools
|
||||||
│ ├── general/ # 🎯 General generators
|
│ ├── general/ # 🎯 General generators
|
||||||
│ ├── bpjs/ # 🏥 BPJS specific tools
|
│ ├── bpjs/ # 🏥 BPJS specific tools
|
||||||
@@ -114,6 +119,10 @@ make migrate-up
|
|||||||
go run cmd/api/main.go
|
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
|
### 3️⃣ Verify Installation
|
||||||
|
|
||||||
@@ -256,16 +265,16 @@ go run tools/general/generate-handler.go orders get post put delete dynamic sear
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Single service
|
# 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
|
# 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**
|
**🩺 Generate SATUSEHAT Handler**
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -91,6 +91,26 @@ type SatuSehatConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetHeader generates required headers for BPJS VClaim API
|
// 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) {
|
func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) {
|
||||||
timenow := time.Now().UTC()
|
timenow := time.Now().UTC()
|
||||||
t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z")
|
t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z")
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cleanResponse removes invalid characters and BOM from the response string
|
|
||||||
|
|
||||||
// VClaimService interface for VClaim operations
|
// VClaimService interface for VClaim operations
|
||||||
type VClaimService interface {
|
type VClaimService interface {
|
||||||
Get(ctx context.Context, endpoint string, result interface{}) error
|
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
|
// 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
|
fullURL := s.config.BaseURL + endpoint
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
@@ -117,11 +115,11 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b
|
|||||||
Str("method", method).
|
Str("method", method).
|
||||||
Str("endpoint", endpoint).
|
Str("endpoint", endpoint).
|
||||||
Msg("Failed to create HTTP request")
|
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
|
// 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("Content-Type", "application/json")
|
||||||
req.Header.Set("X-cons-id", consID)
|
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).
|
Str("user_key", userKey).
|
||||||
Msg("Request headers set")
|
Msg("Request headers set")
|
||||||
|
|
||||||
return req, nil
|
return req, consID, secretKey, tstamp, xSignature, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processResponse processes response from VClaim API
|
// 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()
|
defer res.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
@@ -182,8 +180,7 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error)
|
|||||||
return finalResp, nil
|
return finalResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt response
|
// Decrypt response using the same timestamp from the request
|
||||||
consID, secretKey, _, tstamp, _ := s.config.SetHeader()
|
|
||||||
decryptionKey := consID + secretKey + tstamp
|
decryptionKey := consID + secretKey + tstamp
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
@@ -192,17 +189,6 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error)
|
|||||||
Int("key_length", len(decryptionKey)).
|
Int("key_length", len(decryptionKey)).
|
||||||
Msg("Decryption key components")
|
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)
|
respDecrypt, err := ResponseVclaim(respMentah.Response, decryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to decrypt response")
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -291,7 +277,7 @@ func (s *Service) Put(ctx context.Context, endpoint string, payload interface{},
|
|||||||
|
|
||||||
// Delete performs HTTP DELETE request
|
// Delete performs HTTP DELETE request
|
||||||
func (s *Service) Delete(ctx context.Context, endpoint string, result interface{}) error {
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -338,7 +324,7 @@ func (s *Service) Patch(ctx context.Context, endpoint string, payload interface{
|
|||||||
|
|
||||||
// GetRawResponse returns raw response without mapping
|
// GetRawResponse returns raw response without mapping
|
||||||
func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 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
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
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 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
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
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 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
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
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 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
|
// DeleteRawResponse returns raw response without mapping
|
||||||
func (s *Service) DeleteRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 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
|
// mapToResult maps the final response to the result interface
|
||||||
@@ -498,6 +484,7 @@ func PostRequest(endpoint string, cfg interface{}, data interface{}) interface{}
|
|||||||
|
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanResponse(s string) string {
|
func cleanResponse(s string) string {
|
||||||
// Remove UTF-8 BOM dan variasi BOM lainnya
|
// Remove UTF-8 BOM dan variasi BOM lainnya
|
||||||
s = strings.TrimPrefix(s, "\xef\xbb\xbf") // UTF-8 BOM
|
s = strings.TrimPrefix(s, "\xef\xbb\xbf") // UTF-8 BOM
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -47,6 +48,8 @@ type Logger struct {
|
|||||||
output *log.Logger
|
output *log.Logger
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
jsonFormat bool
|
jsonFormat bool
|
||||||
|
|
||||||
|
logDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogEntry represents a structured log entry
|
// LogEntry represents a structured log entry
|
||||||
@@ -64,12 +67,81 @@ type LogEntry struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new logger instance
|
// 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{
|
return &Logger{
|
||||||
serviceName: serviceName,
|
serviceName: serviceName,
|
||||||
level: level,
|
level: level,
|
||||||
output: log.New(os.Stdout, "", 0),
|
output: log.New(os.Stdout, "", 0),
|
||||||
jsonFormat: jsonFormat,
|
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,
|
level: l.level,
|
||||||
output: l.output,
|
output: l.output,
|
||||||
jsonFormat: l.jsonFormat,
|
jsonFormat: l.jsonFormat,
|
||||||
|
logDir: l.logDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +259,7 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
|
|||||||
level: l.level,
|
level: l.level,
|
||||||
output: l.output,
|
output: l.output,
|
||||||
jsonFormat: l.jsonFormat,
|
jsonFormat: l.jsonFormat,
|
||||||
|
logDir: l.logDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +386,7 @@ func (l *Logger) withField(key string, value interface{}) *Logger {
|
|||||||
level: l.level,
|
level: l.level,
|
||||||
output: l.output,
|
output: l.output,
|
||||||
jsonFormat: l.jsonFormat,
|
jsonFormat: l.jsonFormat,
|
||||||
|
logDir: l.logDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,13 +477,15 @@ func (l *Logger) SaveLogText(entry LogEntry) error {
|
|||||||
logLine += "\n"
|
logLine += "\n"
|
||||||
|
|
||||||
// Buat direktori jika belum ada
|
// Buat direktori jika belum ada
|
||||||
dirPath := "pkg/logger/data"
|
if err := os.MkdirAll(l.logDir, 0755); err != nil {
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tulis ke file
|
// Tulis ke file dengan mutex lock untuk concurrency safety
|
||||||
filePath := dirPath + "/logs.txt"
|
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)
|
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -429,13 +506,15 @@ func (l *Logger) SaveLogJSON(entry LogEntry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Buat direktori jika belum ada
|
// Buat direktori jika belum ada
|
||||||
dirPath := "pkg/logger/data"
|
if err := os.MkdirAll(l.logDir, 0755); err != nil {
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tulis ke file
|
// Tulis ke file dengan mutex lock for concurrency safety
|
||||||
filePath := dirPath + "/logs.json"
|
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)
|
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -458,12 +537,15 @@ func (l *Logger) SaveLogToDatabase(entry LogEntry) error {
|
|||||||
dbLogLine := fmt.Sprintf("DB_LOG: %s|%s|%s|%s\n",
|
dbLogLine := fmt.Sprintf("DB_LOG: %s|%s|%s|%s\n",
|
||||||
entry.Timestamp, entry.Level, entry.Service, entry.Message)
|
entry.Timestamp, entry.Level, entry.Service, entry.Message)
|
||||||
|
|
||||||
dirPath := "pkg/logger/data"
|
if err := os.MkdirAll(l.logDir, 0755); err != nil {
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
return err
|
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)
|
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user