fix(backend): fix AdminService tests and expand CoreHandlers coverage

This commit is contained in:
Tiago Yamamoto 2025-12-28 02:32:57 -03:00
parent 6c87078200
commit a5323a4eaf
7 changed files with 686 additions and 25 deletions

View file

@ -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())
}
})
}

View file

@ -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 := "&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;"
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)
}
}

View file

@ -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)
}
})
}

View file

@ -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()))

View 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)
}

View file

@ -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,

View file

@ -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)
}
}