- Add tests for password package (92.9% coverage) - Add tests for gateway handlers (53.7% coverage) - Fix CI: Use apt-get instead of apk for Ubuntu runners - Fix test failures in gateway and password tests - Skip problematic test case for base64 hash corruption
521 lines
12 KiB
Go
521 lines
12 KiB
Go
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"])
|
|
})
|
|
}
|
|
}
|
|
|