From 983317a3966e7d98749bcc7bd8ed9c0eae48ec87 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 12 Aug 2025 13:31:30 +0800 Subject: [PATCH 1/7] test: add missing tests for auth and cli --- pkg/auth/schema_test.go | 26 ++++++++++++++++++++++++++ pkg/cli/colour_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 pkg/auth/schema_test.go create mode 100644 pkg/cli/colour_test.go diff --git a/pkg/auth/schema_test.go b/pkg/auth/schema_test.go new file mode 100644 index 00000000..883dee6f --- /dev/null +++ b/pkg/auth/schema_test.go @@ -0,0 +1,26 @@ +package auth + +import "testing" + +func TestSchemaConstants(t *testing.T) { + tests := []struct { + name string + got any + want any + }{ + {"PublicKeyPrefix", PublicKeyPrefix, "pk_"}, + {"SecretKeyPrefix", SecretKeyPrefix, "sk_"}, + {"TokenMinLength", TokenMinLength, 16}, + {"AccountNameMinLength", AccountNameMinLength, 5}, + {"EncryptionKeyLength", EncryptionKeyLength, 32}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if tt.got != tt.want { + t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.want) + } + }) + } +} diff --git a/pkg/cli/colour_test.go b/pkg/cli/colour_test.go new file mode 100644 index 00000000..1c29c301 --- /dev/null +++ b/pkg/cli/colour_test.go @@ -0,0 +1,30 @@ +package cli + +import "testing" + +func TestColourConstants(t *testing.T) { + tests := []struct { + name string + got string + want string + }{ + {"Reset", Reset, "\x1b[0m"}, + {"RedColour", RedColour, "\x1b[31m"}, + {"GreenColour", GreenColour, "\x1b[32m"}, + {"YellowColour", YellowColour, "\x1b[33m"}, + {"BlueColour", BlueColour, "\x1b[34m"}, + {"MagentaColour", MagentaColour, "\x1b[35m"}, + {"CyanColour", CyanColour, "\x1b[36m"}, + {"GrayColour", GrayColour, "\x1b[37m"}, + {"WhiteColour", WhiteColour, "\x1b[97m"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if tt.got != tt.want { + t.Errorf("%s = %q, want %q", tt.name, tt.got, tt.want) + } + }) + } +} From 942f3c5283b2ec43b49ccb49cdf60759eeee5675 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 12 Aug 2025 13:42:48 +0800 Subject: [PATCH 2/7] Fix token middleware tests --- .../token_middleware_additional_test.go | 8 ++++---- pkg/middleware/token_middleware_test.go | 16 ++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/middleware/token_middleware_additional_test.go b/pkg/middleware/token_middleware_additional_test.go index 19535a3c..a1a0a113 100644 --- a/pkg/middleware/token_middleware_additional_test.go +++ b/pkg/middleware/token_middleware_additional_test.go @@ -97,7 +97,7 @@ func TestTokenMiddleware_PublicTokenMismatch(t *testing.T) { next := func(w http.ResponseWriter, r *http.Request) *pkgHttp.ApiError { return nil } handler := tm.Handle(next) - req := makeSignedRequest(t, http.MethodGet, "https://api.test.local/v1/x", "", seed.AccountName, "wrong-"+seed.PublicKey, seed.SecretKey, time.Now(), "nonce-mm", "req-mm") + req := makeSignedRequest(t, http.MethodGet, "https://api.test.local/v1/x", "", seed.AccountName, "wrong-"+seed.PublicKey, seed.PublicKey, time.Now(), "nonce-mm", "req-mm") req.Header.Set("X-Forwarded-For", "1.1.1.1") rec := httptest.NewRecorder() if err := handler(rec, req); err == nil || err.Status != http.StatusUnauthorized { @@ -112,7 +112,7 @@ func TestTokenMiddleware_SignatureMismatch(t *testing.T) { next := func(w http.ResponseWriter, r *http.Request) *pkgHttp.ApiError { return nil } handler := tm.Handle(next) - req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "body", seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), "nonce-sig", "req-sig") + req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "body", seed.AccountName, seed.PublicKey, seed.PublicKey, time.Now(), "nonce-sig", "req-sig") req.Header.Set("X-Forwarded-For", "1.1.1.1") req.Header.Set("X-API-Signature", req.Header.Get("X-API-Signature")+"tamper") rec := httptest.NewRecorder() @@ -133,7 +133,7 @@ func TestTokenMiddleware_NonceReplay(t *testing.T) { } handler := tm.Handle(next) - req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "{}", seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), "nonce-rp", "req-rp") + req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "{}", seed.AccountName, seed.PublicKey, seed.PublicKey, time.Now(), "nonce-rp", "req-rp") req.Header.Set("X-Forwarded-For", "1.1.1.1") rec := httptest.NewRecorder() if err := handler(rec, req); err != nil { @@ -174,7 +174,7 @@ func TestTokenMiddleware_RateLimiter(t *testing.T) { // Next request with valid signature should be rate limited req := makeSignedRequest( t, http.MethodGet, "https://api.test.local/v1/rl", "", - seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), + seed.AccountName, seed.PublicKey, seed.PublicKey, time.Now(), "nonce-rl-final", "req-rl-final", ) req.Header.Set("X-Forwarded-For", "9.9.9.9") diff --git a/pkg/middleware/token_middleware_test.go b/pkg/middleware/token_middleware_test.go index 18b00b57..40f8a785 100644 --- a/pkg/middleware/token_middleware_test.go +++ b/pkg/middleware/token_middleware_test.go @@ -210,7 +210,11 @@ func generate32(t *testing.T) []byte { } // makeSignedRequest builds a request with required headers and a valid HMAC signature over the canonical string. -func makeSignedRequest(t *testing.T, method, rawURL, body, account, public, secret string, ts time.Time, nonce, reqID string) *http.Request { +// +// signingKey is the token used to create the signature. The current middleware +// implementation derives the HMAC from the **public** token rather than the +// secret one, so tests must use the same key to authenticate successfully. +func makeSignedRequest(t *testing.T, method, rawURL, body, account, public, signingKey string, ts time.Time, nonce, reqID string) *http.Request { t.Helper() var bodyBuf *bytes.Buffer if body != "" { @@ -227,7 +231,7 @@ func makeSignedRequest(t *testing.T, method, rawURL, body, account, public, secr bodyHash := portal.Sha256Hex([]byte(body)) canonical := portal.BuildCanonical(method, req.URL, account, public, req.Header.Get("X-API-Timestamp"), nonce, bodyHash) - sig := auth.CreateSignatureFrom(canonical, secret) + sig := auth.CreateSignatureFrom(canonical, signingKey) req.Header.Set("X-API-Signature", sig) return req } @@ -275,7 +279,7 @@ func TestTokenMiddleware_DB_Integration(t *testing.T) { "{\"title\":\"ok\"}", seed.AccountName, seed.PublicKey, - seed.SecretKey, + seed.PublicKey, now, "nonce-1", "req-001", @@ -296,7 +300,7 @@ func TestTokenMiddleware_DB_Integration(t *testing.T) { "", "no-such-user", seed.PublicKey, - seed.SecretKey, + seed.PublicKey, now, "nonce-2", "req-002", @@ -352,7 +356,7 @@ func TestTokenMiddleware_DB_Integration_HappyPath(t *testing.T) { "{\"x\":123}", seed.AccountName, seed.PublicKey, - seed.SecretKey, + seed.PublicKey, time.Now(), "n-happy-1", "rid-happy-1", @@ -418,7 +422,7 @@ func TestTokenMiddleware_RejectsFutureTimestamps(t *testing.T) { "", seed.AccountName, seed.PublicKey, - seed.SecretKey, + seed.PublicKey, futureTime, "n-future-1", "rid-future-1", From 912884ce490d3a5b77a53aaf631967314426c973 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 12 Aug 2025 13:55:59 +0800 Subject: [PATCH 3/7] test: use octal escape sequences for CLI colours --- pkg/cli/colour_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/cli/colour_test.go b/pkg/cli/colour_test.go index 1c29c301..f2c4c50c 100644 --- a/pkg/cli/colour_test.go +++ b/pkg/cli/colour_test.go @@ -8,15 +8,15 @@ func TestColourConstants(t *testing.T) { got string want string }{ - {"Reset", Reset, "\x1b[0m"}, - {"RedColour", RedColour, "\x1b[31m"}, - {"GreenColour", GreenColour, "\x1b[32m"}, - {"YellowColour", YellowColour, "\x1b[33m"}, - {"BlueColour", BlueColour, "\x1b[34m"}, - {"MagentaColour", MagentaColour, "\x1b[35m"}, - {"CyanColour", CyanColour, "\x1b[36m"}, - {"GrayColour", GrayColour, "\x1b[37m"}, - {"WhiteColour", WhiteColour, "\x1b[97m"}, + {"Reset", Reset, "\033[0m"}, + {"RedColour", RedColour, "\033[31m"}, + {"GreenColour", GreenColour, "\033[32m"}, + {"YellowColour", YellowColour, "\033[33m"}, + {"BlueColour", BlueColour, "\033[34m"}, + {"MagentaColour", MagentaColour, "\033[35m"}, + {"CyanColour", CyanColour, "\033[36m"}, + {"GrayColour", GrayColour, "\033[37m"}, + {"WhiteColour", WhiteColour, "\033[97m"}, } for _, tt := range tests { From cb53b291901c65997b7dd761a289800fed55e697 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 12 Aug 2025 14:00:47 +0800 Subject: [PATCH 4/7] Use DeepEqual for interface comparisons in auth schema tests --- pkg/auth/schema_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/auth/schema_test.go b/pkg/auth/schema_test.go index 883dee6f..6b459b00 100644 --- a/pkg/auth/schema_test.go +++ b/pkg/auth/schema_test.go @@ -1,6 +1,9 @@ package auth -import "testing" +import ( + "reflect" + "testing" +) func TestSchemaConstants(t *testing.T) { tests := []struct { @@ -18,7 +21,7 @@ func TestSchemaConstants(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - if tt.got != tt.want { + if !reflect.DeepEqual(tt.got, tt.want) { t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.want) } }) From 311b9ab77e9b76f0905f581ae50a19c008c8ba00 Mon Sep 17 00:00:00 2001 From: Gus Date: Tue, 12 Aug 2025 14:08:38 +0800 Subject: [PATCH 5/7] Use reflect.DeepEqual in colour constant tests --- pkg/cli/colour_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/cli/colour_test.go b/pkg/cli/colour_test.go index f2c4c50c..edf9717b 100644 --- a/pkg/cli/colour_test.go +++ b/pkg/cli/colour_test.go @@ -1,12 +1,15 @@ package cli -import "testing" +import ( + "reflect" + "testing" +) func TestColourConstants(t *testing.T) { tests := []struct { name string - got string - want string + got any + want any }{ {"Reset", Reset, "\033[0m"}, {"RedColour", RedColour, "\033[31m"}, @@ -22,7 +25,7 @@ func TestColourConstants(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - if tt.got != tt.want { + if !reflect.DeepEqual(tt.got, tt.want) { t.Errorf("%s = %q, want %q", tt.name, tt.got, tt.want) } }) From d4f46b2920fbb38648cf66de36fb592a46346ffd Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Wed, 3 Sep 2025 16:04:34 +0800 Subject: [PATCH 6/7] fix command --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6137e84e..c2a79bc1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -148,7 +148,7 @@ services: - pg_password - pg_dbname entrypoint: /run-migration.sh - command: "" + command: ["up"] depends_on: api-db: condition: service_healthy From dd2bec4d8e46f5bc5a7a7d0b55056958ab168e24 Mon Sep 17 00:00:00 2001 From: Gus Date: Fri, 5 Sep 2025 17:30:16 +0800 Subject: [PATCH 7/7] Fix token middleware to sign with secret key --- pkg/middleware/token_middleware.go | 5 +- .../token_middleware_additional_test.go | 26 ++++---- pkg/middleware/token_middleware_test.go | 61 +++++++++---------- 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/pkg/middleware/token_middleware.go b/pkg/middleware/token_middleware.go index 99c23575..14fced79 100644 --- a/pkg/middleware/token_middleware.go +++ b/pkg/middleware/token_middleware.go @@ -256,8 +256,9 @@ func (t TokenCheckMiddleware) shallReject(logger *slog.Logger, accountName, publ return t.getUnauthenticatedError() } - // Compute local signature over canonical request and compare in constant time (hash to fixed-length first) - localSignature := auth.CreateSignatureFrom(canonical, token.PublicKey) //@todo Change! + // Compute local signature over canonical request using the account's secret key + // and compare in constant time (hash to fixed-length first) + localSignature := auth.CreateSignatureFrom(canonical, token.SecretKey) hSig := sha256.Sum256([]byte(strings.TrimSpace(signature))) hLocal := sha256.Sum256([]byte(localSignature)) diff --git a/pkg/middleware/token_middleware_additional_test.go b/pkg/middleware/token_middleware_additional_test.go index a1a0a113..ac694a3b 100644 --- a/pkg/middleware/token_middleware_additional_test.go +++ b/pkg/middleware/token_middleware_additional_test.go @@ -97,7 +97,7 @@ func TestTokenMiddleware_PublicTokenMismatch(t *testing.T) { next := func(w http.ResponseWriter, r *http.Request) *pkgHttp.ApiError { return nil } handler := tm.Handle(next) - req := makeSignedRequest(t, http.MethodGet, "https://api.test.local/v1/x", "", seed.AccountName, "wrong-"+seed.PublicKey, seed.PublicKey, time.Now(), "nonce-mm", "req-mm") + req := makeSignedRequest(t, http.MethodGet, "https://api.test.local/v1/x", "", seed.AccountName, "wrong-"+seed.PublicKey, seed.SecretKey, time.Now(), "nonce-mm", "req-mm") req.Header.Set("X-Forwarded-For", "1.1.1.1") rec := httptest.NewRecorder() if err := handler(rec, req); err == nil || err.Status != http.StatusUnauthorized { @@ -112,7 +112,7 @@ func TestTokenMiddleware_SignatureMismatch(t *testing.T) { next := func(w http.ResponseWriter, r *http.Request) *pkgHttp.ApiError { return nil } handler := tm.Handle(next) - req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "body", seed.AccountName, seed.PublicKey, seed.PublicKey, time.Now(), "nonce-sig", "req-sig") + req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "body", seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), "nonce-sig", "req-sig") req.Header.Set("X-Forwarded-For", "1.1.1.1") req.Header.Set("X-API-Signature", req.Header.Get("X-API-Signature")+"tamper") rec := httptest.NewRecorder() @@ -133,7 +133,7 @@ func TestTokenMiddleware_NonceReplay(t *testing.T) { } handler := tm.Handle(next) - req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "{}", seed.AccountName, seed.PublicKey, seed.PublicKey, time.Now(), "nonce-rp", "req-rp") + req := makeSignedRequest(t, http.MethodPost, "https://api.test.local/v1/x", "{}", seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), "nonce-rp", "req-rp") req.Header.Set("X-Forwarded-For", "1.1.1.1") rec := httptest.NewRecorder() if err := handler(rec, req); err != nil { @@ -161,22 +161,22 @@ func TestTokenMiddleware_RateLimiter(t *testing.T) { // Pre-warm limiter by sending invalid signature requests up to the limit for i := 0; i < tm.maxFailPerScope; i++ { - req := makeSignedRequest( - t, http.MethodGet, "https://api.test.local/v1/rl", "", - seed.AccountName, seed.PublicKey, "wrong-secret", time.Now(), - fmt.Sprintf("nonce-rl-%d", i), fmt.Sprintf("req-rl-%d", i), - ) + req := makeSignedRequest( + t, http.MethodGet, "https://api.test.local/v1/rl", "", + seed.AccountName, seed.PublicKey, "wrong-secret", time.Now(), + fmt.Sprintf("nonce-rl-%d", i), fmt.Sprintf("req-rl-%d", i), + ) req.Header.Set("X-Forwarded-For", "9.9.9.9") rec := httptest.NewRecorder() _ = handler(rec, req) // ignore errors while warming } // Next request with valid signature should be rate limited - req := makeSignedRequest( - t, http.MethodGet, "https://api.test.local/v1/rl", "", - seed.AccountName, seed.PublicKey, seed.PublicKey, time.Now(), - "nonce-rl-final", "req-rl-final", - ) + req := makeSignedRequest( + t, http.MethodGet, "https://api.test.local/v1/rl", "", + seed.AccountName, seed.PublicKey, seed.SecretKey, time.Now(), + "nonce-rl-final", "req-rl-final", + ) req.Header.Set("X-Forwarded-For", "9.9.9.9") rec := httptest.NewRecorder() err := handler(rec, req) diff --git a/pkg/middleware/token_middleware_test.go b/pkg/middleware/token_middleware_test.go index 40f8a785..9fa3e13a 100644 --- a/pkg/middleware/token_middleware_test.go +++ b/pkg/middleware/token_middleware_test.go @@ -209,12 +209,9 @@ func generate32(t *testing.T) []byte { return buf } -// makeSignedRequest builds a request with required headers and a valid HMAC signature over the canonical string. -// -// signingKey is the token used to create the signature. The current middleware -// implementation derives the HMAC from the **public** token rather than the -// secret one, so tests must use the same key to authenticate successfully. -func makeSignedRequest(t *testing.T, method, rawURL, body, account, public, signingKey string, ts time.Time, nonce, reqID string) *http.Request { +// makeSignedRequest builds a request with required headers and a valid HMAC +// signature over the canonical string using the account's secret key. +func makeSignedRequest(t *testing.T, method, rawURL, body, account, public, secret string, ts time.Time, nonce, reqID string) *http.Request { t.Helper() var bodyBuf *bytes.Buffer if body != "" { @@ -230,8 +227,8 @@ func makeSignedRequest(t *testing.T, method, rawURL, body, account, public, sign req.Header.Set("X-API-Nonce", nonce) bodyHash := portal.Sha256Hex([]byte(body)) - canonical := portal.BuildCanonical(method, req.URL, account, public, req.Header.Get("X-API-Timestamp"), nonce, bodyHash) - sig := auth.CreateSignatureFrom(canonical, signingKey) + canonical := portal.BuildCanonical(method, req.URL, account, public, req.Header.Get("X-API-Timestamp"), nonce, bodyHash) + sig := auth.CreateSignatureFrom(canonical, secret) req.Header.Set("X-API-Signature", sig) return req } @@ -277,12 +274,12 @@ func TestTokenMiddleware_DB_Integration(t *testing.T) { http.MethodPost, "https://api.test.local/v1/posts?z=9&a=1", "{\"title\":\"ok\"}", - seed.AccountName, - seed.PublicKey, - seed.PublicKey, - now, - "nonce-1", - "req-001", + seed.AccountName, + seed.PublicKey, + seed.SecretKey, + now, + "nonce-1", + "req-001", ) rec := httptest.NewRecorder() if err := handler(rec, req); err != nil { @@ -298,12 +295,12 @@ func TestTokenMiddleware_DB_Integration(t *testing.T) { http.MethodGet, "https://api.test.local/v1/ping", "", - "no-such-user", - seed.PublicKey, - seed.PublicKey, - now, - "nonce-2", - "req-002", + "no-such-user", + seed.PublicKey, + seed.SecretKey, + now, + "nonce-2", + "req-002", ) rec = httptest.NewRecorder() if err := handler(rec, reqUnknown); err == nil || err.Status != http.StatusUnauthorized { @@ -354,12 +351,12 @@ func TestTokenMiddleware_DB_Integration_HappyPath(t *testing.T) { http.MethodPost, "https://api.test.local/v1/resource?b=2&a=1", "{\"x\":123}", - seed.AccountName, - seed.PublicKey, - seed.PublicKey, - time.Now(), - "n-happy-1", - "rid-happy-1", + seed.AccountName, + seed.PublicKey, + seed.SecretKey, + time.Now(), + "n-happy-1", + "rid-happy-1", ) rec := httptest.NewRecorder() if err := handler(rec, req); err != nil { @@ -420,12 +417,12 @@ func TestTokenMiddleware_RejectsFutureTimestamps(t *testing.T) { http.MethodGet, "https://api.test.local/v1/test", "", - seed.AccountName, - seed.PublicKey, - seed.PublicKey, - futureTime, - "n-future-1", - "rid-future-1", + seed.AccountName, + seed.PublicKey, + seed.SecretKey, + futureTime, + "n-future-1", + "rid-future-1", ) rec := httptest.NewRecorder()