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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/api/handlers"
|
"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/domain/entity"
|
||||||
"github.com/rede5/gohorsejobs/backend/internal/core/dto"
|
"github.com/rede5/gohorsejobs/backend/internal/core/dto"
|
||||||
auth "github.com/rede5/gohorsejobs/backend/internal/core/usecases/auth"
|
auth "github.com/rede5/gohorsejobs/backend/internal/core/usecases/auth"
|
||||||
tenant "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant"
|
tenant "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant"
|
||||||
user "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user"
|
user "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user"
|
||||||
|
"github.com/rede5/gohorsejobs/backend/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Mock Implementations ---
|
// --- Mock Implementations ---
|
||||||
|
|
@ -69,7 +75,7 @@ func TestRegisterCandidateHandler_Success(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterCandidateHandler_InvalidPayload(t *testing.T) {
|
func TestRegisterCandidateHandler_InvalidPayload(t *testing.T) {
|
||||||
coreHandlers := createTestCoreHandlers(t, nil)
|
coreHandlers := createTestCoreHandlers(t, nil, nil)
|
||||||
|
|
||||||
body := bytes.NewBufferString("{invalid json}")
|
body := bytes.NewBufferString("{invalid json}")
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", body)
|
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) {
|
func TestRegisterCandidateHandler_MissingFields(t *testing.T) {
|
||||||
coreHandlers := createTestCoreHandlers(t, nil)
|
coreHandlers := createTestCoreHandlers(t, nil, nil)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -130,7 +136,7 @@ func TestRegisterCandidateHandler_MissingFields(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginHandler_InvalidPayload(t *testing.T) {
|
func TestLoginHandler_InvalidPayload(t *testing.T) {
|
||||||
coreHandlers := createTestCoreHandlers(t, nil)
|
coreHandlers := createTestCoreHandlers(t, nil, nil)
|
||||||
|
|
||||||
body := bytes.NewBufferString("{invalid}")
|
body := bytes.NewBufferString("{invalid}")
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", body)
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", body)
|
||||||
|
|
@ -164,7 +170,7 @@ func TestLoginHandler_Success(t *testing.T) {
|
||||||
// Real UseCase with Mocks
|
// Real UseCase with Mocks
|
||||||
loginUC := auth.NewLoginUseCase(mockRepo, mockAuth)
|
loginUC := auth.NewLoginUseCase(mockRepo, mockAuth)
|
||||||
|
|
||||||
coreHandlers := createTestCoreHandlers(t, loginUC)
|
coreHandlers := createTestCoreHandlers(t, nil, loginUC)
|
||||||
|
|
||||||
// Request
|
// Request
|
||||||
payload := dto.LoginRequest{Email: "john@example.com", Password: "123456"}
|
payload := dto.LoginRequest{Email: "john@example.com", Password: "123456"}
|
||||||
|
|
@ -201,9 +207,25 @@ func TestLoginHandler_Success(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// createTestCoreHandlers creates handlers with mocks
|
// createTestCoreHandlers creates handlers with mocks and optional DB
|
||||||
func createTestCoreHandlers(t *testing.T, loginUC *auth.LoginUseCase) *handlers.CoreHandlers {
|
func createTestCoreHandlers(t *testing.T, db *sql.DB, loginUC *auth.LoginUseCase) *handlers.CoreHandlers {
|
||||||
t.Helper()
|
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(
|
return handlers.NewCoreHandlers(
|
||||||
loginUC,
|
loginUC,
|
||||||
(*auth.RegisterCandidateUseCase)(nil),
|
(*auth.RegisterCandidateUseCase)(nil),
|
||||||
|
|
@ -213,10 +235,116 @@ func createTestCoreHandlers(t *testing.T, loginUC *auth.LoginUseCase) *handlers.
|
||||||
(*user.DeleteUserUseCase)(nil),
|
(*user.DeleteUserUseCase)(nil),
|
||||||
(*user.UpdateUserUseCase)(nil),
|
(*user.UpdateUserUseCase)(nil),
|
||||||
(*tenant.ListCompaniesUseCase)(nil),
|
(*tenant.ListCompaniesUseCase)(nil),
|
||||||
nil,
|
auditSvc,
|
||||||
nil,
|
notifSvc,
|
||||||
nil,
|
ticketSvc,
|
||||||
nil,
|
adminSvc,
|
||||||
nil,
|
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
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func TestRateLimiter_isAllowed(t *testing.T) {
|
||||||
limiter := NewRateLimiter(3, time.Minute)
|
limiter := NewRateLimiter(3, time.Minute)
|
||||||
|
|
||||||
|
|
@ -33,7 +84,7 @@ func TestRateLimitMiddleware(t *testing.T) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
middleware := RateLimitMiddleware(2, time.Minute)(handler)
|
mw := RateLimitMiddleware(2, time.Minute)(handler)
|
||||||
|
|
||||||
// Create test requests
|
// Create test requests
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
|
|
@ -41,7 +92,7 @@ func TestRateLimitMiddleware(t *testing.T) {
|
||||||
req.RemoteAddr = "192.168.1.100:12345"
|
req.RemoteAddr = "192.168.1.100:12345"
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
middleware.ServeHTTP(rr, req)
|
mw.ServeHTTP(rr, req)
|
||||||
|
|
||||||
if i < 2 {
|
if i < 2 {
|
||||||
if rr.Code != http.StatusOK {
|
if rr.Code != http.StatusOK {
|
||||||
|
|
@ -60,12 +111,12 @@ func TestSecurityHeadersMiddleware(t *testing.T) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
middleware := SecurityHeadersMiddleware(handler)
|
mw := SecurityHeadersMiddleware(handler)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/test", nil)
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
middleware.ServeHTTP(rr, req)
|
mw.ServeHTTP(rr, req)
|
||||||
|
|
||||||
expectedHeaders := map[string]string{
|
expectedHeaders := map[string]string{
|
||||||
"X-Frame-Options": "DENY",
|
"X-Frame-Options": "DENY",
|
||||||
|
|
@ -86,12 +137,12 @@ func TestAuthMiddleware_NoAuthHeader(t *testing.T) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
middleware := AuthMiddleware(handler)
|
mw := AuthMiddleware(handler)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/test", nil)
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
middleware.ServeHTTP(rr, req)
|
mw.ServeHTTP(rr, req)
|
||||||
|
|
||||||
if rr.Code != http.StatusUnauthorized {
|
if rr.Code != http.StatusUnauthorized {
|
||||||
t.Errorf("Expected status 401, got %d", rr.Code)
|
t.Errorf("Expected status 401, got %d", rr.Code)
|
||||||
|
|
@ -103,13 +154,13 @@ func TestAuthMiddleware_InvalidFormat(t *testing.T) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
middleware := AuthMiddleware(handler)
|
mw := AuthMiddleware(handler)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/test", nil)
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
req.Header.Set("Authorization", "InvalidFormat")
|
req.Header.Set("Authorization", "InvalidFormat")
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
middleware.ServeHTTP(rr, req)
|
mw.ServeHTTP(rr, req)
|
||||||
|
|
||||||
if rr.Code != http.StatusUnauthorized {
|
if rr.Code != http.StatusUnauthorized {
|
||||||
t.Errorf("Expected status 401, got %d", rr.Code)
|
t.Errorf("Expected status 401, got %d", rr.Code)
|
||||||
|
|
@ -121,13 +172,13 @@ func TestAuthMiddleware_InvalidToken(t *testing.T) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
middleware := AuthMiddleware(handler)
|
mw := AuthMiddleware(handler)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/test", nil)
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
req.Header.Set("Authorization", "Bearer invalid.token.here")
|
req.Header.Set("Authorization", "Bearer invalid.token.here")
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
middleware.ServeHTTP(rr, req)
|
mw.ServeHTTP(rr, req)
|
||||||
|
|
||||||
if rr.Code != http.StatusUnauthorized {
|
if rr.Code != http.StatusUnauthorized {
|
||||||
t.Errorf("Expected status 401, got %d", rr.Code)
|
t.Errorf("Expected status 401, got %d", rr.Code)
|
||||||
|
|
@ -139,14 +190,136 @@ func TestRequireRole_NoClaims(t *testing.T) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
middleware := RequireRole("admin")(handler)
|
mw := RequireRole("admin")(handler)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/test", nil)
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
middleware.ServeHTTP(rr, req)
|
mw.ServeHTTP(rr, req)
|
||||||
|
|
||||||
if rr.Code != http.StatusUnauthorized {
|
if rr.Code != http.StatusUnauthorized {
|
||||||
t.Errorf("Expected status 401, got %d", rr.Code)
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAdminService_ListCompanies(t *testing.T) {
|
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").
|
mock.ExpectQuery("INSERT INTO applications").
|
||||||
WithArgs(
|
WithArgs(
|
||||||
req.JobID, req.UserID, req.Name, req.Phone, req.LineID, req.WhatsApp, req.Email,
|
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"}).
|
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
AddRow("app-789", time.Now(), time.Now()))
|
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
|
FROM jobs WHERE id = $1
|
||||||
`
|
`
|
||||||
err := s.DB.QueryRow(query, id).Scan(
|
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.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.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,
|
&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