fix(backend): fix AdminService tests and expand CoreHandlers coverage
This commit is contained in:
parent
6c87078200
commit
a5323a4eaf
7 changed files with 686 additions and 25 deletions
|
|
@ -3,17 +3,23 @@ package handlers_test
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/rede5/gohorsejobs/backend/internal/api/handlers"
|
||||
"github.com/rede5/gohorsejobs/backend/internal/api/middleware"
|
||||
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
|
||||
"github.com/rede5/gohorsejobs/backend/internal/core/dto"
|
||||
auth "github.com/rede5/gohorsejobs/backend/internal/core/usecases/auth"
|
||||
tenant "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant"
|
||||
user "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user"
|
||||
"github.com/rede5/gohorsejobs/backend/internal/services"
|
||||
)
|
||||
|
||||
// --- Mock Implementations ---
|
||||
|
|
@ -69,7 +75,7 @@ func TestRegisterCandidateHandler_Success(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRegisterCandidateHandler_InvalidPayload(t *testing.T) {
|
||||
coreHandlers := createTestCoreHandlers(t, nil)
|
||||
coreHandlers := createTestCoreHandlers(t, nil, nil)
|
||||
|
||||
body := bytes.NewBufferString("{invalid json}")
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", body)
|
||||
|
|
@ -84,7 +90,7 @@ func TestRegisterCandidateHandler_InvalidPayload(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRegisterCandidateHandler_MissingFields(t *testing.T) {
|
||||
coreHandlers := createTestCoreHandlers(t, nil)
|
||||
coreHandlers := createTestCoreHandlers(t, nil, nil)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
|
@ -130,7 +136,7 @@ func TestRegisterCandidateHandler_MissingFields(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoginHandler_InvalidPayload(t *testing.T) {
|
||||
coreHandlers := createTestCoreHandlers(t, nil)
|
||||
coreHandlers := createTestCoreHandlers(t, nil, nil)
|
||||
|
||||
body := bytes.NewBufferString("{invalid}")
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", body)
|
||||
|
|
@ -164,7 +170,7 @@ func TestLoginHandler_Success(t *testing.T) {
|
|||
// Real UseCase with Mocks
|
||||
loginUC := auth.NewLoginUseCase(mockRepo, mockAuth)
|
||||
|
||||
coreHandlers := createTestCoreHandlers(t, loginUC)
|
||||
coreHandlers := createTestCoreHandlers(t, nil, loginUC)
|
||||
|
||||
// Request
|
||||
payload := dto.LoginRequest{Email: "john@example.com", Password: "123456"}
|
||||
|
|
@ -201,9 +207,25 @@ func TestLoginHandler_Success(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// createTestCoreHandlers creates handlers with mocks
|
||||
func createTestCoreHandlers(t *testing.T, loginUC *auth.LoginUseCase) *handlers.CoreHandlers {
|
||||
// createTestCoreHandlers creates handlers with mocks and optional DB
|
||||
func createTestCoreHandlers(t *testing.T, db *sql.DB, loginUC *auth.LoginUseCase) *handlers.CoreHandlers {
|
||||
t.Helper()
|
||||
|
||||
// Init services if DB provided
|
||||
var auditSvc *services.AuditService
|
||||
var notifSvc *services.NotificationService
|
||||
var ticketSvc *services.TicketService
|
||||
var adminSvc *services.AdminService
|
||||
var credSvc *services.CredentialsService
|
||||
|
||||
if db != nil {
|
||||
auditSvc = services.NewAuditService(db)
|
||||
notifSvc = services.NewNotificationService(db, nil)
|
||||
ticketSvc = services.NewTicketService(db)
|
||||
adminSvc = services.NewAdminService(db)
|
||||
credSvc = services.NewCredentialsService(db)
|
||||
}
|
||||
|
||||
return handlers.NewCoreHandlers(
|
||||
loginUC,
|
||||
(*auth.RegisterCandidateUseCase)(nil),
|
||||
|
|
@ -213,10 +235,116 @@ func createTestCoreHandlers(t *testing.T, loginUC *auth.LoginUseCase) *handlers.
|
|||
(*user.DeleteUserUseCase)(nil),
|
||||
(*user.UpdateUserUseCase)(nil),
|
||||
(*tenant.ListCompaniesUseCase)(nil),
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
auditSvc,
|
||||
notifSvc,
|
||||
ticketSvc,
|
||||
adminSvc,
|
||||
credSvc,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCoreHandlers_ListNotifications(t *testing.T) {
|
||||
// Setup DB Mock
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Setup Handlers with DB
|
||||
handlers := createTestCoreHandlers(t, db, nil)
|
||||
|
||||
// User ID
|
||||
userID := "user-123"
|
||||
|
||||
// Mock DB Query for ListNotifications
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, type, title, message, link, read_at, created_at, updated_at FROM notifications`)).
|
||||
WithArgs(userID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "type", "title", "message", "link", "read_at", "created_at", "updated_at"}).
|
||||
AddRow("1", userID, "info", "Welcome", "Hello", nil, nil, time.Now(), time.Now()))
|
||||
|
||||
// Request
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications", nil)
|
||||
|
||||
// Inject Context
|
||||
ctx := context.WithValue(req.Context(), middleware.ContextUserID, userID)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// Execute
|
||||
handlers.ListNotifications(rec, req)
|
||||
|
||||
// Assert
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
|
||||
// Check Body (simple check)
|
||||
if !bytes.Contains(rec.Body.Bytes(), []byte("Welcome")) {
|
||||
t.Errorf("Expected body to contain 'Welcome'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreHandlers_Tickets(t *testing.T) {
|
||||
userID := "user-123"
|
||||
|
||||
t.Run("CreateTicket", func(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
handlers := createTestCoreHandlers(t, db, nil)
|
||||
|
||||
// Mock Insert: user_id, subject, priority
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO tickets`)).
|
||||
WithArgs(userID, "Issue", "low").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}).
|
||||
AddRow("ticket-1", userID, "Issue", "open", "low", time.Now(), time.Now()))
|
||||
|
||||
payload := map[string]string{
|
||||
"subject": "Issue",
|
||||
"message": "Help",
|
||||
"priority": "low",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/support/tickets", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
ctx := context.WithValue(req.Context(), middleware.ContextUserID, userID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handlers.CreateTicket(rec, req.WithContext(ctx))
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Errorf("CreateTicket status = %d, want %d. Body: %s", rec.Code, http.StatusCreated, rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListTickets", func(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
handlers := createTestCoreHandlers(t, db, nil)
|
||||
|
||||
// Mock Select
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets`)).
|
||||
WithArgs(userID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}).
|
||||
AddRow("ticket-1", userID, "Issue", "open", "low", time.Now(), time.Now()))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/support/tickets", nil)
|
||||
ctx := context.WithValue(req.Context(), middleware.ContextUserID, userID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handlers.ListTickets(rec, req.WithContext(ctx))
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("ListTickets status = %d, want %d. Body: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,63 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rede5/gohorsejobs/backend/internal/utils"
|
||||
)
|
||||
|
||||
func TestLoggingMiddleware(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
mw := LoggingMiddleware(handler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Success(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := r.Context().Value(UserKey).(*utils.Claims)
|
||||
if !ok {
|
||||
t.Error("Claims not found in context")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if claims.UserID != 1 {
|
||||
t.Errorf("Expected userID 1, got %d", claims.UserID)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
mw := AuthMiddleware(handler)
|
||||
|
||||
token, _ := utils.GenerateJWT(1, "test-user", "user")
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiter_isAllowed(t *testing.T) {
|
||||
limiter := NewRateLimiter(3, time.Minute)
|
||||
|
||||
|
|
@ -33,7 +84,7 @@ func TestRateLimitMiddleware(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
middleware := RateLimitMiddleware(2, time.Minute)(handler)
|
||||
mw := RateLimitMiddleware(2, time.Minute)(handler)
|
||||
|
||||
// Create test requests
|
||||
for i := 0; i < 3; i++ {
|
||||
|
|
@ -41,7 +92,7 @@ func TestRateLimitMiddleware(t *testing.T) {
|
|||
req.RemoteAddr = "192.168.1.100:12345"
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
middleware.ServeHTTP(rr, req)
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
if i < 2 {
|
||||
if rr.Code != http.StatusOK {
|
||||
|
|
@ -60,12 +111,12 @@ func TestSecurityHeadersMiddleware(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
middleware := SecurityHeadersMiddleware(handler)
|
||||
mw := SecurityHeadersMiddleware(handler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
middleware.ServeHTTP(rr, req)
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
expectedHeaders := map[string]string{
|
||||
"X-Frame-Options": "DENY",
|
||||
|
|
@ -86,12 +137,12 @@ func TestAuthMiddleware_NoAuthHeader(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
middleware := AuthMiddleware(handler)
|
||||
mw := AuthMiddleware(handler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
middleware.ServeHTTP(rr, req)
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", rr.Code)
|
||||
|
|
@ -103,13 +154,13 @@ func TestAuthMiddleware_InvalidFormat(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
middleware := AuthMiddleware(handler)
|
||||
mw := AuthMiddleware(handler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Authorization", "InvalidFormat")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
middleware.ServeHTTP(rr, req)
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", rr.Code)
|
||||
|
|
@ -121,13 +172,13 @@ func TestAuthMiddleware_InvalidToken(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
middleware := AuthMiddleware(handler)
|
||||
mw := AuthMiddleware(handler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Authorization", "Bearer invalid.token.here")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
middleware.ServeHTTP(rr, req)
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", rr.Code)
|
||||
|
|
@ -139,14 +190,136 @@ func TestRequireRole_NoClaims(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
middleware := RequireRole("admin")(handler)
|
||||
mw := RequireRole("admin")(handler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
middleware.ServeHTTP(rr, req)
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSMiddleware(t *testing.T) {
|
||||
os.Setenv("CORS_ORIGINS", "http://allowed.com,http://another.com")
|
||||
defer os.Unsetenv("CORS_ORIGINS")
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
mw := CORSMiddleware(handler)
|
||||
|
||||
// Test allowed origin
|
||||
req := httptest.NewRequest("OPTIONS", "/test", nil)
|
||||
req.Header.Set("Origin", "http://allowed.com")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Header().Get("Access-Control-Allow-Origin") != "http://allowed.com" {
|
||||
t.Errorf("Expected allow origin http://allowed.com, got %s", rr.Header().Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
|
||||
// Test disallowed origin
|
||||
req = httptest.NewRequest("OPTIONS", "/test", nil)
|
||||
req.Header.Set("Origin", "http://hacker.com")
|
||||
rr = httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Header().Get("Access-Control-Allow-Origin") != "" {
|
||||
t.Errorf("Expected empty allow origin, got %s", rr.Header().Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeMiddleware(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Read body to verify sanitization
|
||||
var body map[string]interface{}
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
w.Header().Set("X-Sanitized-Name", body["name"].(string))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
mw := SanitizeMiddleware(handler)
|
||||
|
||||
jsonBody := `{"name": "<script>alert('xss')</script>"}`
|
||||
req := httptest.NewRequest("POST", "/test", strings.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
expected := "<script>alert('xss')</script>"
|
||||
if rr.Header().Get("X-Sanitized-Name") != expected {
|
||||
t.Errorf("Expected sanitized name %s, got %s", expected, rr.Header().Get("X-Sanitized-Name"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireRole_Success(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
mw := RequireRole("admin")(handler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
// Inject claims into context manually to simulate authenticated user
|
||||
claims := &utils.Claims{
|
||||
UserID: 1,
|
||||
Role: "admin",
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), UserKey, claims)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(rr, req.WithContext(ctx))
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireRole_Forbidden(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
mw := RequireRole("admin")(handler)
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
claims := &utils.Claims{
|
||||
UserID: 1,
|
||||
Role: "user", // Wrong role
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), UserKey, claims)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(rr, req.WithContext(ctx))
|
||||
|
||||
if rr.Code != http.StatusForbidden {
|
||||
t.Errorf("Expected status 403, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeMiddleware_InvalidJSON(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
mw := SanitizeMiddleware(handler)
|
||||
|
||||
jsonBody := `{"name": "broken json`
|
||||
req := httptest.NewRequest("POST", "/test", strings.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(rr, req)
|
||||
|
||||
// Should pass through if JSON invalid (or handle gracefully)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200 (pass through), got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ package services
|
|||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
||||
)
|
||||
|
||||
func TestAdminService_ListCompanies(t *testing.T) {
|
||||
|
|
@ -280,3 +282,126 @@ func TestAdminService_UpdateCompanyStatus(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminService_ListUsers(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
service := NewAdminService(db)
|
||||
|
||||
t.Run("returns users list", func(t *testing.T) {
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM users`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(2))
|
||||
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, COALESCE(name, full_name, identifier, ''), email, role, COALESCE(status, 'active'), created_at FROM users`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "email", "role", "status", "created_at"}).
|
||||
AddRow("1", "User 1", "u1@test.com", "admin", "active", time.Now()).
|
||||
AddRow("2", "User 2", "u2@test.com", "user", "inactive", time.Now()))
|
||||
|
||||
users, count, err := service.ListUsers(context.Background(), 1, 10, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("ListUsers() error = %v", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Errorf("ListUsers() count = %d, want 2", count)
|
||||
}
|
||||
if len(users) != 2 {
|
||||
t.Errorf("ListUsers() length = %d, want 2", len(users))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminService_DuplicateJob(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
service := NewAdminService(db)
|
||||
jobID := "job-123"
|
||||
|
||||
// Expect fetch original job
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT company_id`)).
|
||||
WithArgs(jobID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"company_id", "created_by", "title", "description", "salary_min", "salary_max", "salary_type",
|
||||
"employment_type", "work_mode", "working_hours", "location", "region_id", "city_id",
|
||||
"requirements", "benefits", "visa_support", "language_level",
|
||||
}).AddRow(
|
||||
"comp-1", "user-1", "Job 1", "Desc", 100, 200, "m",
|
||||
"ft", "remote", "40h", "Remote", 0, 0,
|
||||
nil, nil, false, "N2",
|
||||
))
|
||||
|
||||
// Expect insert duplicated job
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO jobs`)).
|
||||
WithArgs(
|
||||
"comp-1", "user-1", "Job 1", "Desc", 100.0, 200.0, "m",
|
||||
"ft", "remote", "40h", "Remote", 0, 0,
|
||||
sqlmock.AnyArg(), sqlmock.AnyArg(), false, "N2",
|
||||
"draft", false, sqlmock.AnyArg(), sqlmock.AnyArg(), // Status, IsFeatured, CreatedAt, UpdatedAt
|
||||
).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).
|
||||
AddRow("new-job-id"))
|
||||
|
||||
job, err := service.DuplicateJob(context.Background(), jobID)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("DuplicateJob() error = %v", err)
|
||||
}
|
||||
if job.ID != "new-job-id" {
|
||||
t.Errorf("DuplicateJob() ID = %s, want new-job-id", job.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminService_EmailTemplates(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
service := NewAdminService(db)
|
||||
|
||||
t.Run("ListEmailTemplates", func(t *testing.T) {
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "slug", "subject", "body_html", "variables", "created_at", "updated_at"}).
|
||||
AddRow("1", "welcome", "Welcome", "<h1>Hi</h1>", "{name}", time.Now(), time.Now()))
|
||||
|
||||
templates, err := service.ListEmailTemplates(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("ListEmailTemplates() error = %v", err)
|
||||
}
|
||||
if len(templates) != 1 {
|
||||
t.Errorf("ListEmailTemplates() len = %d, want 1", len(templates))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CreateEmailTemplate", func(t *testing.T) {
|
||||
// Expect insert
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO email_templates`)).
|
||||
WithArgs("new-template", "Subject", "Body", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at"}). // Implementation returns id, created_at
|
||||
AddRow("2", time.Now()))
|
||||
|
||||
req := dto.CreateEmailTemplateRequest{
|
||||
Slug: "new-template",
|
||||
Subject: "Subject",
|
||||
BodyHTML: "Body",
|
||||
Variables: []string{"name"},
|
||||
}
|
||||
|
||||
created, err := service.CreateEmailTemplate(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Errorf("CreateEmailTemplate() error = %v", err)
|
||||
}
|
||||
if created.Slug != "new-template" {
|
||||
t.Errorf("CreateEmailTemplate() slug = %s, want new-template", created.Slug)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ func TestApplicationService_CreateApplication(t *testing.T) {
|
|||
mock.ExpectQuery("INSERT INTO applications").
|
||||
WithArgs(
|
||||
req.JobID, req.UserID, req.Name, req.Phone, req.LineID, req.WhatsApp, req.Email,
|
||||
req.Message, req.ResumeURL, req.Documents, "pending", sqlmock.AnyArg(), sqlmock.AnyArg(),
|
||||
req.Message, req.ResumeURL, sqlmock.AnyArg(), "pending", sqlmock.AnyArg(), sqlmock.AnyArg(),
|
||||
).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||
AddRow("app-789", time.Now(), time.Now()))
|
||||
|
|
|
|||
136
backend/internal/services/credentials_service_test.go
Normal file
136
backend/internal/services/credentials_service_test.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package services_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/rede5/gohorsejobs/backend/internal/services"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Helper to generate a valid RSA private key for testing
|
||||
func generateTestRSAKey() (string, *rsa.PublicKey, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
keyBytes := x509.MarshalPKCS1PrivateKey(key)
|
||||
pemBlock := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: keyBytes,
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(pemBlock)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(pemBytes), &key.PublicKey, nil
|
||||
}
|
||||
|
||||
func TestSaveCredentials(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
assert.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
service := services.NewCredentialsService(db)
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO external_services_credentials`)).
|
||||
WithArgs("stripe", "encrypted_data", "admin").
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
err = service.SaveCredentials(ctx, "stripe", "encrypted_data", "admin")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetDecryptedKey(t *testing.T) {
|
||||
// Setup RSA Key
|
||||
privKeyStr, pubKey, err := generateTestRSAKey()
|
||||
assert.NoError(t, err)
|
||||
os.Setenv("RSA_PRIVATE_KEY_BASE64", privKeyStr)
|
||||
defer os.Unsetenv("RSA_PRIVATE_KEY_BASE64")
|
||||
|
||||
db, mock, err := sqlmock.New()
|
||||
assert.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
service := services.NewCredentialsService(db)
|
||||
ctx := context.Background()
|
||||
|
||||
// Encrypt a secret
|
||||
secret := "my-secret-key"
|
||||
encryptedBytes, err := rsa.EncryptOAEP(
|
||||
sha256.New(),
|
||||
rand.Reader,
|
||||
pubKey,
|
||||
[]byte(secret),
|
||||
nil,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
encryptedPayload := base64.StdEncoding.EncodeToString(encryptedBytes)
|
||||
|
||||
// Mock DB return
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT encrypted_payload FROM external_services_credentials`)).
|
||||
WithArgs("stripe").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"encrypted_payload"}).AddRow(encryptedPayload))
|
||||
|
||||
// Execute
|
||||
decrypted, err := service.GetDecryptedKey(ctx, "stripe")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, secret, decrypted)
|
||||
}
|
||||
|
||||
func TestListConfiguredServices(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
assert.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
service := services.NewCredentialsService(db)
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT service_name, updated_at, COALESCE(updated_by::text, '')`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"service_name", "updated_at", "updated_by"}).
|
||||
AddRow("stripe", time.Now().Format(time.RFC3339), "admin").
|
||||
AddRow("appwrite", time.Now().Format(time.RFC3339), "system"))
|
||||
|
||||
servicesList, err := service.ListConfiguredServices(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, servicesList)
|
||||
|
||||
// Verify mapped correctly
|
||||
foundStripe := false
|
||||
for _, s := range servicesList {
|
||||
if s.ServiceName == "stripe" {
|
||||
assert.True(t, s.IsConfigured)
|
||||
foundStripe = true
|
||||
}
|
||||
if s.ServiceName == "firebase" {
|
||||
assert.False(t, s.IsConfigured)
|
||||
}
|
||||
}
|
||||
assert.True(t, foundStripe)
|
||||
}
|
||||
|
||||
func TestDeleteCredentials(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
assert.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
service := services.NewCredentialsService(db)
|
||||
ctx := context.Background()
|
||||
|
||||
mock.ExpectExec(regexp.QuoteMeta(`DELETE FROM external_services_credentials`)).
|
||||
WithArgs("stripe").
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
err = service.DeleteCredentials(ctx, "stripe")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
@ -245,7 +245,6 @@ func (s *JobService) GetJobByID(id string) (*models.Job, error) {
|
|||
FROM jobs WHERE id = $1
|
||||
`
|
||||
err := s.DB.QueryRow(query, id).Scan(
|
||||
&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
|
||||
&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
|
||||
&j.EmploymentType, &j.WorkingHours, &j.Location, &j.RegionID, &j.CityID, &j.SalaryNegotiable,
|
||||
&j.Requirements, &j.Benefits, &j.VisaSupport, &j.LanguageLevel, &j.Status, &j.CreatedAt, &j.UpdatedAt,
|
||||
|
|
|
|||
|
|
@ -125,3 +125,103 @@ func TestGetJobs(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
func TestGetJobByID(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
service := services.NewJobService(db)
|
||||
|
||||
jobID := "100"
|
||||
|
||||
// Mocking row for GetJobByID (20 columns)
|
||||
rows := sqlmock.NewRows([]string{
|
||||
"id", "company_id", "title", "description", "salary_min", "salary_max", "salary_type",
|
||||
"employment_type", "working_hours", "location", "region_id", "city_id", "salary_negotiable",
|
||||
"requirements", "benefits", "visa_support", "language_level", "status", "created_at", "updated_at",
|
||||
}).AddRow(
|
||||
jobID, 1, "Title", "Desc", 100, 200, "m",
|
||||
"ft", "40h", "Remote", 0, 0, false,
|
||||
nil, nil, false, "N2", "open", time.Now(), time.Now(),
|
||||
)
|
||||
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, company_id`)).
|
||||
WithArgs(jobID).
|
||||
WillReturnRows(rows)
|
||||
|
||||
job, err := service.GetJobByID(jobID)
|
||||
if err != nil {
|
||||
t.Errorf("GetJobByID() error = %v", err)
|
||||
return
|
||||
}
|
||||
if job == nil || job.ID != jobID {
|
||||
t.Errorf("GetJobByID() got = %v, want ID %s", job, jobID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateJob(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
service := services.NewJobService(db)
|
||||
jobID := "100"
|
||||
newTitle := "New Title"
|
||||
|
||||
req := dto.UpdateJobRequest{
|
||||
Title: &newTitle,
|
||||
}
|
||||
|
||||
// Expect UPDATE with RETURNING
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`UPDATE jobs SET title = $1, updated_at = NOW() WHERE id = $2 RETURNING id, updated_at`)).
|
||||
WithArgs(newTitle, jobID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "updated_at"}).AddRow(jobID, time.Now()))
|
||||
|
||||
// Expect subsequent SELECT to fetch updated job (20 columns)
|
||||
rows := sqlmock.NewRows([]string{
|
||||
"id", "company_id", "title", "description", "salary_min", "salary_max", "salary_type",
|
||||
"employment_type", "working_hours", "location", "region_id", "city_id", "salary_negotiable",
|
||||
"requirements", "benefits", "visa_support", "language_level", "status", "created_at", "updated_at",
|
||||
}).AddRow(
|
||||
jobID, 1, newTitle, "Desc", 100, 200, "m",
|
||||
"ft", "40h", "Remote", 0, 0, false,
|
||||
nil, nil, false, "N2", "open", time.Now(), time.Now(),
|
||||
)
|
||||
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, company_id`)).
|
||||
WithArgs(jobID).
|
||||
WillReturnRows(rows)
|
||||
|
||||
job, err := service.UpdateJob(jobID, req)
|
||||
if err != nil {
|
||||
t.Errorf("UpdateJob() error = %v", err)
|
||||
return
|
||||
}
|
||||
if job.Title != newTitle {
|
||||
t.Errorf("UpdateJob() title = %s, want %s", job.Title, newTitle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteJob(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
service := services.NewJobService(db)
|
||||
jobID := "100"
|
||||
|
||||
mock.ExpectExec(regexp.QuoteMeta(`DELETE FROM jobs WHERE id = $1`)).
|
||||
WithArgs(jobID).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
err = service.DeleteJob(jobID)
|
||||
if err != nil {
|
||||
t.Errorf("DeleteJob() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue