diff --git a/services/api/go.mod b/services/api/go.mod index 0383a87..832608c 100644 --- a/services/api/go.mod +++ b/services/api/go.mod @@ -2,29 +2,37 @@ module github.com/libpulse/platform/services/api go 1.25.5 +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/stretchr/testify v1.11.1 +) + require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect - github.com/gin-contrib/cors v1.7.6 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.11.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect @@ -37,4 +45,5 @@ require ( golang.org/x/text v0.27.0 // indirect golang.org/x/tools v0.34.0 // indirect google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/services/api/go.sum b/services/api/go.sum index fdc2f54..5ac5246 100644 --- a/services/api/go.sum +++ b/services/api/go.sum @@ -4,10 +4,10 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= @@ -16,30 +16,35 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -47,18 +52,25 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -85,5 +97,8 @@ golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/services/api/internal/handlers/me.go b/services/api/internal/handlers/me.go index ca2d04e..52936e0 100644 --- a/services/api/internal/handlers/me.go +++ b/services/api/internal/handlers/me.go @@ -6,29 +6,43 @@ import ( "github.com/gin-gonic/gin" "github.com/libpulse/platform/services/api/internal/auth" - "github.com/libpulse/platform/services/api/internal/supabase" "github.com/libpulse/platform/services/api/internal/utils/errors" ) -type ErrorResponse struct { - Error string `json:"error"` - Code string `json:"code"` -} - // Get User info when authenticated: /api/v1/me -func GetCurrentUserHandler(sb *supabase.Client) gin.HandlerFunc { +func GetCurrentUserHandler(store UserStore) gin.HandlerFunc { return func(c *gin.Context) { - // Get claims from context (set by auth middleware) - claimsAny, _ := c.Get(auth.ContextKeyClaims) - claims := claimsAny.(*auth.SupabaseClaims) + // 1) ensure claims(injected by auth middleware) + claimsAny, ok := c.Get(auth.ContextKeyClaims) + if !ok { + apiErr := errors.NewAPIError(errors.ErrUnauthorized) + c.JSON(apiErr.StatusCode(), apiErr) + return + } + + // 2) Check the claims type + claims, ok := claimsAny.(*auth.SupabaseClaims) + if !ok || claims.Subject == "" { + apiErr := errors.NewAPIError(errors.ErrUnauthorized) + c.JSON(apiErr.StatusCode(), apiErr) + return + } - user, err := sb.GetUserByID(c.Request.Context(), claims.Subject) + // 3) Get user info + user, err := store.GetUserByID(c.Request.Context(), claims.Subject) if err != nil { apiErr := errors.NewAPIError(errors.ErrInternalError) c.JSON(apiErr.StatusCode(), apiErr) return } + // 4) Defensive: store returned (nil, nil), which should never happen. + if user == nil { + apiErr := errors.NewAPIError(errors.ErrInternalError) + c.JSON(apiErr.StatusCode(), apiErr) + return + } + c.JSON(http.StatusOK, user) } } diff --git a/services/api/internal/handlers/me_test.go b/services/api/internal/handlers/me_test.go new file mode 100644 index 0000000..42e1b1b --- /dev/null +++ b/services/api/internal/handlers/me_test.go @@ -0,0 +1,223 @@ +package handlers + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "github.com/libpulse/platform/services/api/internal/auth" + "github.com/libpulse/platform/services/api/internal/supabase" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// MockUserStore implements handlers.UserStore for testing. +type MockUserStore struct { + mock.Mock +} + +// NewMockUserStore creates a new mock UserStore. +func NewMockUserStore() *MockUserStore { + return &MockUserStore{} +} + +// GetUserByID mocks UserStore.GetUserByID. +func (m *MockUserStore) GetUserByID( + ctx context.Context, + userID string, +) (*supabase.User, error) { + args := m.Called(ctx, userID) + + var user *supabase.User + if v := args.Get(0); v != nil { + user = v.(*supabase.User) + } + + return user, args.Error(1) +} + +// TestGetCurrentUserHandler_Success tests successful user retrieval +func TestGetCurrentUserHandler_Success(t *testing.T) { + gin.SetMode(gin.TestMode) + mockStore := NewMockUserStore() + + userID := "user-123-abc" + userData := &supabase.User{ + ID: userID, + Email: "test@example.com", + } + + // Mock Supabase response + mockStore.On("GetUserByID", mock.Anything, userID).Return(userData, nil) + + // Create HTTP test context + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/me", nil) + + // Create claims with Subject field + claims := &auth.SupabaseClaims{ + Email: "test@example.com", + Role: "admin", + RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID, + }, + } + c.Set(auth.ContextKeyClaims, claims) + + // Execute handler + handler := GetCurrentUserHandler(mockStore) + handler(c) + + // Assertions + assert.Equal(t, http.StatusOK, w.Code) + assert.Contains(t, w.Body.String(), "test@example.com") + mockStore.AssertExpectations(t) +} + +// TestGetCurrentUserHandler_DatabaseError tests error handling +func TestGetCurrentUserHandler_DatabaseError(t *testing.T) { + gin.SetMode(gin.TestMode) + mockStore := NewMockUserStore() + + userID := "user-123-abc" + + // Mock Supabase error + mockStore.On("GetUserByID", mock.Anything, userID).Return(nil, errors.New("database error")) + + // Create HTTP test context + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/me", nil) + + claims := &auth.SupabaseClaims{ + Email: "test@example.com", + Role: "admin", + RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID, + }, + } + c.Set(auth.ContextKeyClaims, claims) + + // Execute handler + handler := GetCurrentUserHandler(mockStore) + handler(c) + + // Assertions + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "internal_error") + mockStore.AssertExpectations(t) +} + +// TestGetCurrentUserHandler_NoClaimsInContext tests when claims aren't set +func TestGetCurrentUserHandler_NoClaimsInContext(t *testing.T) { + gin.SetMode(gin.TestMode) + mockStore := NewMockUserStore() + + // Create HTTP test context WITHOUT setting claims + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + handler := GetCurrentUserHandler(mockStore) + handler(c) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Contains(t, w.Body.String(), "unauthorized") + mockStore.AssertNotCalled(t, "GetUserByID", mock.Anything, mock.Anything) +} + +// TestGetCurrentUserHandler_EmptyUserID tests with empty user ID +func TestGetCurrentUserHandler_EmptyUserID(t *testing.T) { + gin.SetMode(gin.TestMode) + mockStore := NewMockUserStore() + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/me", nil) + + // Create claims with empty user id. + claims := &auth.SupabaseClaims{ + Email: "test@example.com", + Role: "admin", + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "", + }, + } + c.Set(auth.ContextKeyClaims, claims) + + handler := GetCurrentUserHandler(mockStore) + handler(c) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + mockStore.AssertNotCalled(t, "GetUserByID", mock.Anything, mock.Anything) +} + +// TestGetCurrentUserHandler_InvalidClaimsType tests the case where an invalid claims type exists; +// This should return 401. +func TestGetCurrentUserHandler_InvalidClaimsType(t *testing.T) { + gin.SetMode(gin.TestMode) + mockStore := NewMockUserStore() + + // Create HTTP test context + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/me", nil) + + // Put a wrong type into context under the claims key + c.Set(auth.ContextKeyClaims, "not-a-claims-struct") + + // Execute handler + handler := GetCurrentUserHandler(mockStore) + handler(c) + + // Assertions: should be unauthorized + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Contains(t, w.Body.String(), "unauthorized") + + // Ensure store isn't called + mockStore.AssertNotCalled(t, "GetUserByID", mock.Anything, mock.Anything) + mockStore.AssertExpectations(t) +} + +// TestGetCurrentUserHandler_NilUserNoError tests the case where the store returns (nil, nil). +// This should be treated as an internal error (contract violation). +func TestGetCurrentUserHandler_NilUserNoError(t *testing.T) { + gin.SetMode(gin.TestMode) + mockStore := NewMockUserStore() + + userID := "user-123-abc" + + // Create HTTP test context + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/me", nil) + + // Set valid claims + claims := &auth.SupabaseClaims{ + Email: "test@example.com", + Role: "admin", + RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID, + }, + } + c.Set(auth.ContextKeyClaims, claims) + + // Mock store returning (nil, nil) — contract violation + mockStore.On("GetUserByID", c.Request.Context(), userID). + Return((*supabase.User)(nil), nil) + + // Execute handler + handler := GetCurrentUserHandler(mockStore) + handler(c) + + // What SHOULD happen: + // returning (nil, nil) should be treated as internal error + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "internal_error") + + mockStore.AssertExpectations(t) +} diff --git a/services/api/internal/handlers/user_store.go b/services/api/internal/handlers/user_store.go new file mode 100644 index 0000000..290d9be --- /dev/null +++ b/services/api/internal/handlers/user_store.go @@ -0,0 +1,12 @@ +package handlers + +import ( + "context" + + "github.com/libpulse/platform/services/api/internal/supabase" +) + +// UserStore abstracts user data access for handlers, enabling dependency injection and unit testing. +type UserStore interface { + GetUserByID(ctx context.Context, id string) (*supabase.User, error) +} diff --git a/services/api/internal/supabase/user_store.go b/services/api/internal/supabase/user_store.go new file mode 100644 index 0000000..5fda7d8 --- /dev/null +++ b/services/api/internal/supabase/user_store.go @@ -0,0 +1,14 @@ +package supabase + +import "context" + +// UserStore is a thin wrapper around Client that provides user-related data access. +// It is used as the concrete implementation injected into handlers. +type UserStore struct { + Client *Client +} + +// GetUserByID delegates to the underlying Supabase client. +func (s *UserStore) GetUserByID(ctx context.Context, id string) (*User, error) { + return s.Client.GetUserByID(ctx, id) +} diff --git a/services/api/main.go b/services/api/main.go index 41dfa11..953b00a 100644 --- a/services/api/main.go +++ b/services/api/main.go @@ -74,9 +74,12 @@ func main() { // Protected API routes api := r.Group("/api/v1") + // Create Adapter Store + userStore := &supabase.UserStore{Client: sbClient} + api.Use(auth.NewMiddleware(cfg.JWTSecret)) { - api.GET("/me", handlers.GetCurrentUserHandler(sbClient)) + api.GET("/me", handlers.GetCurrentUserHandler(userStore)) } addr := ":8080"