diff --git a/backend/internal/services/admin_service_test.go b/backend/internal/services/admin_service_test.go new file mode 100644 index 0000000..4c5e149 --- /dev/null +++ b/backend/internal/services/admin_service_test.go @@ -0,0 +1,183 @@ +package services + +import ( + "context" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestAdminService_ListCompanies(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewAdminService(db) + ctx := context.Background() + + t.Run("returns empty list when no companies", func(t *testing.T) { + // Count query + mock.ExpectQuery("SELECT COUNT").WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + // Data query + mock.ExpectQuery("SELECT id, name, slug").WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "slug", "type", "document", "address", "region_id", "city_id", + "phone", "email", "website", "logo_url", "description", "active", "verified", + "created_at", "updated_at", + })) + + companies, total, err := service.ListCompanies(ctx, nil, 1, 10) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if total != 0 { + t.Errorf("Expected total=0, got %d", total) + } + if len(companies) != 0 { + t.Errorf("Expected 0 companies, got %d", len(companies)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) + + t.Run("filters by verified status", func(t *testing.T) { + verified := true + mock.ExpectQuery("SELECT COUNT").WithArgs(verified).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + mock.ExpectQuery("SELECT id, name, slug").WithArgs(verified, 10, 0).WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "slug", "type", "document", "address", "region_id", "city_id", + "phone", "email", "website", "logo_url", "description", "active", "verified", + "created_at", "updated_at", + })) + + _, _, err := service.ListCompanies(ctx, &verified, 1, 10) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) +} + +func TestAdminService_ListTags(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewAdminService(db) + ctx := context.Background() + + t.Run("returns empty list when no tags", func(t *testing.T) { + mock.ExpectQuery("SELECT id, name, category, active, created_at, updated_at FROM job_tags"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "category", "active", "created_at", "updated_at"})) + + tags, err := service.ListTags(ctx, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(tags) != 0 { + t.Errorf("Expected 0 tags, got %d", len(tags)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) + + t.Run("filters by category", func(t *testing.T) { + category := "stack" + mock.ExpectQuery("SELECT id, name, category, active, created_at, updated_at FROM job_tags WHERE category"). + WithArgs(category). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "category", "active", "created_at", "updated_at"})) + + _, err := service.ListTags(ctx, &category) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) +} + +func TestAdminService_CreateTag(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewAdminService(db) + ctx := context.Background() + + t.Run("creates a new tag", func(t *testing.T) { + mock.ExpectQuery("INSERT INTO job_tags"). + WithArgs("Go", "stack", true, sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) + + tag, err := service.CreateTag(ctx, "Go", "stack") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if tag == nil { + t.Fatal("Expected tag, got nil") + } + if tag.Name != "Go" { + t.Errorf("Expected name='Go', got '%s'", tag.Name) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) + + t.Run("rejects empty tag name", func(t *testing.T) { + _, err := service.CreateTag(ctx, " ", "stack") + if err == nil { + t.Error("Expected error for empty tag name") + } + }) +} + +func TestAdminService_GetUser(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewAdminService(db) + ctx := context.Background() + + t.Run("returns user by id", func(t *testing.T) { + userID := "019b5290-9680-7c06-9ee3-c9e0e117251b" + now := time.Now() + mock.ExpectQuery("SELECT id, name, email, role, created_at FROM users WHERE id"). + WithArgs(userID). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "email", "role", "created_at"}). + AddRow(userID, "Test User", "test@example.com", "admin", now)) + + user, err := service.GetUser(ctx, userID) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if user == nil { + t.Fatal("Expected user, got nil") + } + if user.Name != "Test User" { + t.Errorf("Expected name='Test User', got '%s'", user.Name) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) +} diff --git a/backend/internal/services/audit_service_test.go b/backend/internal/services/audit_service_test.go new file mode 100644 index 0000000..9e0106f --- /dev/null +++ b/backend/internal/services/audit_service_test.go @@ -0,0 +1,108 @@ +package services + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestAuditService_RecordLogin(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewAuditService(db) + ctx := context.Background() + + t.Run("records login successfully", func(t *testing.T) { + ipAddress := "192.168.1.1" + userAgent := "Mozilla/5.0" + + mock.ExpectExec("INSERT INTO login_audit"). + WithArgs("user-123", "test@example.com", "admin,recruiter", &ipAddress, &userAgent). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err := service.RecordLogin(ctx, LoginAuditInput{ + UserID: "user-123", + Identifier: "test@example.com", + Roles: []string{"admin", "recruiter"}, + IPAddress: &ipAddress, + UserAgent: &userAgent, + }) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) + + t.Run("records login without optional fields", func(t *testing.T) { + mock.ExpectExec("INSERT INTO login_audit"). + WithArgs("user-456", "admin@test.com", "superadmin", nil, nil). + WillReturnResult(sqlmock.NewResult(2, 1)) + + err := service.RecordLogin(ctx, LoginAuditInput{ + UserID: "user-456", + Identifier: "admin@test.com", + Roles: []string{"superadmin"}, + IPAddress: nil, + UserAgent: nil, + }) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) +} + +func TestAuditService_ListLogins(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewAuditService(db) + ctx := context.Background() + + t.Run("returns empty list when no audits", func(t *testing.T) { + mock.ExpectQuery("SELECT id, user_id, identifier, roles, ip_address, user_agent, created_at FROM login_audit"). + WithArgs(50). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "identifier", "roles", "ip_address", "user_agent", "created_at"})) + + audits, err := service.ListLogins(ctx, 0) // Should default to 50 + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(audits) != 0 { + t.Errorf("Expected 0 audits, got %d", len(audits)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) + + t.Run("respects custom limit", func(t *testing.T) { + mock.ExpectQuery("SELECT id, user_id, identifier, roles, ip_address, user_agent, created_at FROM login_audit"). + WithArgs(10). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "identifier", "roles", "ip_address", "user_agent", "created_at"})) + + _, err := service.ListLogins(ctx, 10) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) +} diff --git a/backend/internal/services/notification_service_test.go b/backend/internal/services/notification_service_test.go new file mode 100644 index 0000000..af85ce2 --- /dev/null +++ b/backend/internal/services/notification_service_test.go @@ -0,0 +1,119 @@ +package services + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestNotificationService_ListNotifications(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewNotificationService(db) + ctx := context.Background() + userID := "019b5290-9680-7c06-9ee3-c9e0e117251b" + + t.Run("returns empty list when no notifications", func(t *testing.T) { + mock.ExpectQuery("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"})) + + notifications, err := service.ListNotifications(ctx, userID) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(notifications) != 0 { + t.Errorf("Expected 0 notifications, got %d", len(notifications)) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) +} + +func TestNotificationService_CreateNotification(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewNotificationService(db) + ctx := context.Background() + userID := "019b5290-9680-7c06-9ee3-c9e0e117251b" + + t.Run("creates a new notification", func(t *testing.T) { + mock.ExpectExec("INSERT INTO notifications"). + WithArgs(userID, "info", "Test Title", "Test message", nil). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err := service.CreateNotification(ctx, userID, "info", "Test Title", "Test message", nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) +} + +func TestNotificationService_MarkAsRead(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewNotificationService(db) + ctx := context.Background() + userID := "019b5290-9680-7c06-9ee3-c9e0e117251b" + + t.Run("marks notification as read", func(t *testing.T) { + mock.ExpectExec("UPDATE notifications SET read_at"). + WithArgs("1", userID). + WillReturnResult(sqlmock.NewResult(0, 1)) + + err := service.MarkAsRead(ctx, "1", userID) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) +} + +func TestNotificationService_MarkAllAsRead(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create mock db: %v", err) + } + defer db.Close() + + service := NewNotificationService(db) + ctx := context.Background() + userID := "019b5290-9680-7c06-9ee3-c9e0e117251b" + + t.Run("marks all notifications as read", func(t *testing.T) { + mock.ExpectExec("UPDATE notifications SET read_at"). + WithArgs(userID). + WillReturnResult(sqlmock.NewResult(0, 5)) + + err := service.MarkAllAsRead(ctx, userID) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("Unmet expectations: %v", err) + } + }) +}