package gateway import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "git.dcentral.systems/toolz/goplt/pkg/services" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func TestGateway_handleLogin(t *testing.T) { t.Parallel() tests := []struct { name string requestBody interface{} clientResp *services.TokenResponse clientErr error expectedStatus int expectedBody func(*testing.T, *httptest.ResponseRecorder) }{ { name: "successful login", requestBody: map[string]string{ "email": "test@example.com", "password": "password123", }, clientResp: &services.TokenResponse{ AccessToken: "access-token", RefreshToken: "refresh-token", ExpiresIn: 3600, TokenType: "Bearer", }, clientErr: nil, expectedStatus: http.StatusOK, expectedBody: func(t *testing.T, w *httptest.ResponseRecorder) { var resp services.TokenResponse err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, "access-token", resp.AccessToken) assert.Equal(t, "refresh-token", resp.RefreshToken) }, }, { name: "invalid request body", requestBody: map[string]string{ "email": "test@example.com", // missing password }, expectedStatus: http.StatusBadRequest, }, { name: "client error - unauthorized", requestBody: map[string]string{ "email": "test@example.com", "password": "wrongpassword", }, clientErr: status.Error(codes.Unauthenticated, "invalid credentials"), expectedStatus: http.StatusUnauthorized, }, { name: "client error - internal", requestBody: map[string]string{ "email": "test@example.com", "password": "password123", }, clientErr: status.Error(codes.Internal, "internal error"), expectedStatus: http.StatusInternalServerError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &mockAuthClient{ loginResp: tt.clientResp, loginErr: tt.clientErr, } gateway := &Gateway{ log: &mockLogger{}, } router := gin.New() router.POST("/login", func(c *gin.Context) { gateway.handleLogin(context.Background(), c, mockClient) }) bodyBytes, _ := json.Marshal(tt.requestBody) req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code) if tt.expectedBody != nil { tt.expectedBody(t, w) } }) } } func TestGateway_handleRefreshToken(t *testing.T) { t.Parallel() tests := []struct { name string requestBody interface{} clientResp *services.TokenResponse clientErr error expectedStatus int }{ { name: "successful refresh", requestBody: map[string]string{ "refresh_token": "refresh-token-123", }, clientResp: &services.TokenResponse{ AccessToken: "new-access-token", RefreshToken: "new-refresh-token", ExpiresIn: 3600, TokenType: "Bearer", }, expectedStatus: http.StatusOK, }, { name: "invalid request body", requestBody: map[string]string{ // missing refresh_token }, expectedStatus: http.StatusBadRequest, }, { name: "client error", requestBody: map[string]string{ "refresh_token": "invalid-token", }, clientErr: status.Error(codes.Unauthenticated, "invalid token"), expectedStatus: http.StatusUnauthorized, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &mockAuthClient{ refreshResp: tt.clientResp, refreshErr: tt.clientErr, } gateway := &Gateway{ log: &mockLogger{}, } router := gin.New() router.POST("/refresh", func(c *gin.Context) { gateway.handleRefreshToken(context.Background(), c, mockClient) }) bodyBytes, _ := json.Marshal(tt.requestBody) req := httptest.NewRequest(http.MethodPost, "/refresh", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code) }) } } func TestGateway_handleValidateToken(t *testing.T) { t.Parallel() tests := []struct { name string requestBody interface{} clientResp *services.TokenClaims clientErr error expectedStatus int }{ { name: "successful validation", requestBody: map[string]string{ "token": "valid-token", }, clientResp: &services.TokenClaims{ UserID: "user-123", Email: "test@example.com", Roles: []string{"user"}, ExpiresAt: 1234567890, }, expectedStatus: http.StatusOK, }, { name: "invalid request body", requestBody: map[string]string{ // missing token }, expectedStatus: http.StatusBadRequest, }, { name: "client error - invalid token", requestBody: map[string]string{ "token": "invalid-token", }, clientErr: status.Error(codes.Unauthenticated, "invalid token"), expectedStatus: http.StatusUnauthorized, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &mockAuthClient{ validateResp: tt.clientResp, validateErr: tt.clientErr, } gateway := &Gateway{ log: &mockLogger{}, } router := gin.New() router.POST("/validate", func(c *gin.Context) { gateway.handleValidateToken(context.Background(), c, mockClient) }) bodyBytes, _ := json.Marshal(tt.requestBody) req := httptest.NewRequest(http.MethodPost, "/validate", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code) }) } } func TestGateway_handleLogout(t *testing.T) { t.Parallel() tests := []struct { name string requestBody interface{} clientErr error expectedStatus int }{ { name: "successful logout", requestBody: map[string]string{ "refresh_token": "refresh-token-123", }, expectedStatus: http.StatusOK, }, { name: "invalid request body", requestBody: map[string]string{ // missing refresh_token }, expectedStatus: http.StatusBadRequest, }, { name: "client error", requestBody: map[string]string{ "refresh_token": "invalid-token", }, clientErr: status.Error(codes.NotFound, "token not found"), expectedStatus: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &mockAuthClient{ logoutErr: tt.clientErr, } gateway := &Gateway{ log: &mockLogger{}, } router := gin.New() router.POST("/logout", func(c *gin.Context) { gateway.handleLogout(context.Background(), c, mockClient) }) bodyBytes, _ := json.Marshal(tt.requestBody) req := httptest.NewRequest(http.MethodPost, "/logout", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code) }) } } func TestGateway_handleGetUser(t *testing.T) { t.Parallel() tests := []struct { name string userID string clientResp *services.User clientErr error expectedStatus int }{ { name: "successful get user", userID: "user-123", clientResp: &services.User{ ID: "user-123", Email: "test@example.com", Username: "testuser", FirstName: "Test", LastName: "User", }, expectedStatus: http.StatusOK, }, { name: "client error - not found", userID: "nonexistent", clientErr: status.Error(codes.NotFound, "user not found"), expectedStatus: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &mockIdentityClient{ getUserResp: tt.clientResp, getUserErr: tt.clientErr, } gateway := &Gateway{ log: &mockLogger{}, } router := gin.New() router.GET("/users/:id", func(c *gin.Context) { gateway.handleGetUser(context.Background(), c, mockClient, tt.userID) }) req := httptest.NewRequest(http.MethodGet, "/users/"+tt.userID, nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code) }) } } func TestGateway_handleCreateUser(t *testing.T) { t.Parallel() tests := []struct { name string requestBody interface{} clientResp *services.User clientErr error expectedStatus int }{ { name: "successful create", requestBody: services.CreateUserRequest{ Email: "test@example.com", Username: "testuser", Password: "password123", FirstName: "Test", LastName: "User", }, clientResp: &services.User{ ID: "user-123", Email: "test@example.com", Username: "testuser", FirstName: "Test", LastName: "User", }, expectedStatus: http.StatusCreated, }, { name: "invalid JSON", requestBody: "not a json object", expectedStatus: http.StatusBadRequest, }, { name: "client error - already exists", requestBody: services.CreateUserRequest{ Email: "existing@example.com", Username: "existing", Password: "password123", }, clientErr: status.Error(codes.AlreadyExists, "user already exists"), expectedStatus: http.StatusConflict, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &mockIdentityClient{ createUserResp: tt.clientResp, createUserErr: tt.clientErr, } gateway := &Gateway{ log: &mockLogger{}, } router := gin.New() router.POST("/users", func(c *gin.Context) { gateway.handleCreateUser(context.Background(), c, mockClient) }) bodyBytes, _ := json.Marshal(tt.requestBody) req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code) }) } } func TestGateway_handleGRPCError(t *testing.T) { t.Parallel() tests := []struct { name string err error expectedStatus int expectedError string }{ { name: "Unauthenticated", err: status.Error(codes.Unauthenticated, "invalid token"), expectedStatus: http.StatusUnauthorized, expectedError: "Unauthorized", }, { name: "PermissionDenied", err: status.Error(codes.PermissionDenied, "access denied"), expectedStatus: http.StatusForbidden, expectedError: "Forbidden", }, { name: "NotFound", err: status.Error(codes.NotFound, "resource not found"), expectedStatus: http.StatusNotFound, expectedError: "Not found", }, { name: "InvalidArgument", err: status.Error(codes.InvalidArgument, "invalid input"), expectedStatus: http.StatusBadRequest, expectedError: "Invalid request", }, { name: "AlreadyExists", err: status.Error(codes.AlreadyExists, "resource exists"), expectedStatus: http.StatusConflict, expectedError: "Resource already exists", }, { name: "Internal", err: status.Error(codes.Internal, "internal error"), expectedStatus: http.StatusInternalServerError, expectedError: "Internal server error", }, { name: "Unavailable", err: status.Error(codes.Unavailable, "service unavailable"), expectedStatus: http.StatusServiceUnavailable, expectedError: "Service unavailable", }, { name: "non-gRPC error", err: assert.AnError, expectedStatus: http.StatusInternalServerError, expectedError: "Internal server error", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gateway := &Gateway{ log: &mockLogger{}, } router := gin.New() router.GET("/test", func(c *gin.Context) { gateway.handleGRPCError(c, tt.err) }) req := httptest.NewRequest(http.MethodGet, "/test", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, tt.expectedError, response["error"]) }) } }