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

View file

@ -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 := "&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 (
"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)
}
})
}

View file

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

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

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