Skip to content

Commit c7ce03c

Browse files
authored
test: Add API gateway and internal account unit tests (#1922)
* test: Add API gateway and internal account unit tests - auth_builder_test.go: BuildAuthMiddleware with JWKS URL, local signer, API keys, and error cases - auth/composite_validator_test.go: CompositeJWTValidator with empty, single, multi-validator scenarios - internal/middleware/response_recorder_test.go: responseRecorder defaults, idempotency, buffer pool behavior - internal-account/domain/valuation_feature_test.go: re-export constant and error alias verification - internal-account/adapters/persistence/valuation_feature_repository_test.go: constructor and CRUD via shared implementation * fix: Remove unnecessary type conversion in valuation feature repository test * fix: Address CodeRabbit feedback - rename misleading test names --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent dfd99de commit c7ce03c

5 files changed

Lines changed: 502 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package auth
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
platformauth "github.com/meridianhub/meridian/shared/platform/auth"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestNewCompositeJWTValidator(t *testing.T) {
13+
v1 := &mockValidator{claims: &platformauth.Claims{UserID: "user-1"}}
14+
v2 := &mockValidator{claims: &platformauth.Claims{UserID: "user-2"}}
15+
16+
composite := NewCompositeJWTValidator(v1, v2)
17+
18+
assert.NotNil(t, composite)
19+
assert.Len(t, composite.validators, 2)
20+
}
21+
22+
func TestCompositeJWTValidator_EmptyValidators_ReturnsError(t *testing.T) {
23+
composite := NewCompositeJWTValidator()
24+
25+
claims, err := composite.ValidateToken("any-token")
26+
27+
assert.Nil(t, claims)
28+
assert.ErrorIs(t, err, platformauth.ErrInvalidToken)
29+
}
30+
31+
func TestCompositeJWTValidator_FirstValidatorSucceeds(t *testing.T) {
32+
expectedClaims := &platformauth.Claims{
33+
UserID: "user-1",
34+
TenantID: "acme_corp",
35+
}
36+
v1 := &mockValidator{claims: expectedClaims}
37+
v2 := &mockValidator{claims: &platformauth.Claims{UserID: "user-2"}}
38+
39+
composite := NewCompositeJWTValidator(v1, v2)
40+
41+
claims, err := composite.ValidateToken("some-token")
42+
43+
require.NoError(t, err)
44+
assert.Equal(t, expectedClaims, claims)
45+
}
46+
47+
func TestCompositeJWTValidator_FirstFailsSecondSucceeds(t *testing.T) {
48+
expectedClaims := &platformauth.Claims{
49+
UserID: "user-2",
50+
TenantID: "beta_corp",
51+
}
52+
v1 := &mockValidator{err: platformauth.ErrInvalidToken}
53+
v2 := &mockValidator{claims: expectedClaims}
54+
55+
composite := NewCompositeJWTValidator(v1, v2)
56+
57+
claims, err := composite.ValidateToken("some-token")
58+
59+
require.NoError(t, err)
60+
assert.Equal(t, expectedClaims, claims)
61+
}
62+
63+
func TestCompositeJWTValidator_AllFail_ReturnsLastError(t *testing.T) {
64+
firstErr := errors.New("first validator error")
65+
lastErr := errors.New("last validator error")
66+
v1 := &mockValidator{err: firstErr}
67+
v2 := &mockValidator{err: lastErr}
68+
69+
composite := NewCompositeJWTValidator(v1, v2)
70+
71+
claims, err := composite.ValidateToken("invalid-token")
72+
73+
assert.Nil(t, claims)
74+
assert.ErrorIs(t, err, lastErr)
75+
}
76+
77+
func TestCompositeJWTValidator_SingleValidator_Succeeds(t *testing.T) {
78+
expectedClaims := &platformauth.Claims{UserID: "solo-user"}
79+
v := &mockValidator{claims: expectedClaims}
80+
81+
composite := NewCompositeJWTValidator(v)
82+
83+
claims, err := composite.ValidateToken("token")
84+
85+
require.NoError(t, err)
86+
assert.Equal(t, expectedClaims, claims)
87+
}
88+
89+
func TestCompositeJWTValidator_SingleValidator_Fails(t *testing.T) {
90+
v := &mockValidator{err: platformauth.ErrInvalidToken}
91+
92+
composite := NewCompositeJWTValidator(v)
93+
94+
claims, err := composite.ValidateToken("expired-token")
95+
96+
assert.Nil(t, claims)
97+
assert.ErrorIs(t, err, platformauth.ErrInvalidToken)
98+
}
99+
100+
func TestCompositeJWTValidator_ThreeValidators_ThirdSucceeds(t *testing.T) {
101+
expectedClaims := &platformauth.Claims{UserID: "third-user"}
102+
errFirst := errors.New("first fails")
103+
errSecond := errors.New("second fails")
104+
v1 := &mockValidator{err: errFirst}
105+
v2 := &mockValidator{err: errSecond}
106+
v3 := &mockValidator{claims: expectedClaims}
107+
108+
composite := NewCompositeJWTValidator(v1, v2, v3)
109+
110+
claims, err := composite.ValidateToken("token")
111+
112+
require.NoError(t, err)
113+
assert.Equal(t, expectedClaims, claims)
114+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package gateway
2+
3+
import (
4+
"io"
5+
"log/slog"
6+
"testing"
7+
8+
platformauth "github.com/meridianhub/meridian/shared/platform/auth"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestBuildAuthMiddleware_WithJWKSURL(t *testing.T) {
14+
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
15+
16+
config := AuthConfig{
17+
JWKSURL: "https://example.com/.well-known/jwks.json",
18+
}
19+
20+
// NewJWKSProvider defers initial fetch, so any non-empty URL succeeds at creation.
21+
middleware, err := BuildAuthMiddleware(config, logger)
22+
23+
require.NoError(t, err)
24+
assert.NotNil(t, middleware)
25+
middleware.Close()
26+
}
27+
28+
func TestBuildAuthMiddleware_EmptyJWKSURL(t *testing.T) {
29+
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
30+
31+
config := AuthConfig{
32+
JWKSURL: "", // empty URL should fail
33+
}
34+
35+
_, err := BuildAuthMiddleware(config, logger)
36+
37+
assert.Error(t, err)
38+
assert.Contains(t, err.Error(), "failed to create JWKS provider")
39+
}
40+
41+
func TestBuildAuthMiddleware_WithLocalSigner(t *testing.T) {
42+
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
43+
44+
signer, err := platformauth.NewJWTSigner(platformauth.JWTSignerConfig{})
45+
require.NoError(t, err)
46+
47+
config := AuthConfig{
48+
JWKSURL: "https://example.com/.well-known/jwks.json",
49+
}
50+
51+
middleware, err := BuildAuthMiddleware(config, logger, signer)
52+
53+
require.NoError(t, err)
54+
assert.NotNil(t, middleware)
55+
middleware.Close()
56+
}
57+
58+
func TestBuildAuthMiddleware_WithAPIKeys(t *testing.T) {
59+
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
60+
61+
config := AuthConfig{
62+
JWKSURL: "https://example.com/.well-known/jwks.json",
63+
APIKeys: map[string]string{
64+
"api-key-1": "service-a",
65+
"api-key-2": "service-b",
66+
},
67+
RateLimitPerSecond: 100,
68+
RateLimitBurst: 200,
69+
}
70+
71+
middleware, err := BuildAuthMiddleware(config, logger)
72+
73+
require.NoError(t, err)
74+
assert.NotNil(t, middleware)
75+
middleware.Close()
76+
}
77+
78+
func TestBuildAuthMiddleware_CloseIsIdempotent(t *testing.T) {
79+
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
80+
81+
config := AuthConfig{
82+
JWKSURL: "https://example.com/.well-known/jwks.json",
83+
APIKeys: map[string]string{"key": "identity"},
84+
}
85+
86+
middleware, err := BuildAuthMiddleware(config, logger)
87+
require.NoError(t, err)
88+
89+
// Close should be safe to call multiple times.
90+
assert.NotPanics(t, func() {
91+
middleware.Close()
92+
middleware.Close()
93+
})
94+
}
95+
96+
func TestBuildAuthMiddleware_WithLocalSigner_NilSignerIgnored(t *testing.T) {
97+
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
98+
99+
config := AuthConfig{
100+
JWKSURL: "https://example.com/.well-known/jwks.json",
101+
}
102+
103+
// Passing a nil signer should fall back to dex-only path.
104+
middleware, err := BuildAuthMiddleware(config, logger, nil)
105+
106+
require.NoError(t, err)
107+
assert.NotNil(t, middleware)
108+
middleware.Close()
109+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package middleware
2+
3+
import (
4+
"bytes"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestNewResponseRecorder_Defaults(t *testing.T) {
13+
rec := newResponseRecorder()
14+
defer rec.release()
15+
16+
assert.Equal(t, http.StatusOK, rec.code)
17+
assert.NotNil(t, rec.headers)
18+
assert.NotNil(t, rec.buf)
19+
assert.False(t, rec.wroteHeader)
20+
}
21+
22+
func TestResponseRecorder_WriteHeader_Idempotent(t *testing.T) {
23+
rec := newResponseRecorder()
24+
defer rec.release()
25+
26+
rec.WriteHeader(http.StatusCreated)
27+
rec.WriteHeader(http.StatusBadRequest) // second call is a no-op
28+
29+
assert.Equal(t, http.StatusCreated, rec.code)
30+
assert.True(t, rec.wroteHeader)
31+
}
32+
33+
func TestResponseRecorder_Write_SetsWroteHeader(t *testing.T) {
34+
rec := newResponseRecorder()
35+
defer rec.release()
36+
37+
assert.False(t, rec.wroteHeader)
38+
39+
_, err := rec.Write([]byte("hello"))
40+
require.NoError(t, err)
41+
42+
assert.True(t, rec.wroteHeader)
43+
assert.Equal(t, http.StatusOK, rec.code)
44+
}
45+
46+
func TestResponseRecorder_Write_AccumulatesBody(t *testing.T) {
47+
rec := newResponseRecorder()
48+
defer rec.release()
49+
50+
_, err := rec.Write([]byte("foo"))
51+
require.NoError(t, err)
52+
_, err = rec.Write([]byte("bar"))
53+
require.NoError(t, err)
54+
55+
assert.Equal(t, "foobar", rec.buf.String())
56+
}
57+
58+
func TestResponseRecorder_Header_ReturnsHeaders(t *testing.T) {
59+
rec := newResponseRecorder()
60+
defer rec.release()
61+
62+
rec.Header().Set("X-Custom", "value")
63+
64+
assert.Equal(t, "value", rec.headers.Get("X-Custom"))
65+
assert.Equal(t, rec.headers, rec.Header())
66+
}
67+
68+
func TestAcquireBuffer_ReturnsEmptyBuffer(t *testing.T) {
69+
b := acquireBuffer()
70+
defer releaseBuffer(b)
71+
72+
assert.Equal(t, 0, b.Len())
73+
}
74+
75+
func TestReleaseBuffer_ReturnedBufferIsReset(t *testing.T) {
76+
b := acquireBuffer()
77+
_, err := b.WriteString("some data")
78+
require.NoError(t, err)
79+
80+
releaseBuffer(b)
81+
82+
// Acquire again - should get a reset buffer (may or may not be the same object).
83+
b2 := acquireBuffer()
84+
defer releaseBuffer(b2)
85+
assert.Equal(t, 0, b2.Len())
86+
}
87+
88+
func TestReleaseBuffer_LargeBuffer_DoesNotPanic(t *testing.T) {
89+
// Create a buffer larger than 64KB threshold.
90+
// releaseBuffer drops oversized buffers instead of returning them to the pool.
91+
large := make([]byte, 65*1024)
92+
b := &bytes.Buffer{}
93+
_, err := b.Write(large)
94+
require.NoError(t, err)
95+
assert.Greater(t, b.Cap(), 64*1024)
96+
97+
assert.NotPanics(t, func() {
98+
releaseBuffer(b)
99+
})
100+
}

0 commit comments

Comments
 (0)