diff --git a/backend/internal/api/handlers/core_handlers_test.go b/backend/internal/api/handlers/core_handlers_test.go new file mode 100644 index 0000000..cb71882 --- /dev/null +++ b/backend/internal/api/handlers/core_handlers_test.go @@ -0,0 +1,175 @@ +package handlers_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/rede5/gohorsejobs/backend/internal/api/handlers" + "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" +) + +// --- Mock Implementations --- + +type mockUserRepo struct { + saveFunc func(user interface{}) (interface{}, error) + findByEmailFunc func(email string) (interface{}, error) +} + +func (m *mockUserRepo) Save(ctx interface{}, user interface{}) (interface{}, error) { + if m.saveFunc != nil { + return m.saveFunc(user) + } + return user, nil +} + +func (m *mockUserRepo) FindByEmail(ctx interface{}, email string) (interface{}, error) { + if m.findByEmailFunc != nil { + return m.findByEmailFunc(email) + } + return nil, nil +} + +func (m *mockUserRepo) FindByID(ctx interface{}, id string) (interface{}, error) { return nil, nil } +func (m *mockUserRepo) FindAllByTenant(ctx interface{}, tenantID string, l, o int) ([]interface{}, int, error) { + return nil, 0, nil +} +func (m *mockUserRepo) Update(ctx interface{}, user interface{}) (interface{}, error) { + return nil, nil +} +func (m *mockUserRepo) Delete(ctx interface{}, id string) error { return nil } + +type mockAuthService struct{} + +func (m *mockAuthService) HashPassword(password string) (string, error) { + return "hashed_" + password, nil +} +func (m *mockAuthService) GenerateToken(userID, tenantID string, roles []string) (string, error) { + return "mock_token", nil +} +func (m *mockAuthService) VerifyPassword(hash, password string) bool { return true } +func (m *mockAuthService) ValidateToken(token string) (map[string]interface{}, error) { + return nil, nil +} + +// --- Test Cases --- + +func TestRegisterCandidateHandler_Success(t *testing.T) { + // This is a simplified integration test structure + // In production, you'd wire up the full dependency injection + t.Skip("Integration test requires full DI setup - use unit tests in usecases/auth instead") +} + +func TestRegisterCandidateHandler_InvalidPayload(t *testing.T) { + // Create a minimal handler for testing payload validation + coreHandlers := createTestCoreHandlers(t) + if coreHandlers == nil { + t.Skip("Cannot create test handlers - skipping") + return + } + + // Test with invalid JSON + body := bytes.NewBufferString("{invalid json}") + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", body) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + coreHandlers.RegisterCandidate(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Errorf("Expected status %d, got %d", http.StatusBadRequest, rec.Code) + } +} + +func TestRegisterCandidateHandler_MissingFields(t *testing.T) { + coreHandlers := createTestCoreHandlers(t) + if coreHandlers == nil { + t.Skip("Cannot create test handlers - skipping") + return + } + + testCases := []struct { + name string + payload dto.RegisterCandidateRequest + wantCode int + }{ + { + name: "Missing Email", + payload: dto.RegisterCandidateRequest{Name: "John", Password: "123456"}, + wantCode: http.StatusBadRequest, + }, + { + name: "Missing Password", + payload: dto.RegisterCandidateRequest{Name: "John", Email: "john@example.com"}, + wantCode: http.StatusBadRequest, + }, + { + name: "Missing Name", + payload: dto.RegisterCandidateRequest{Email: "john@example.com", Password: "123456"}, + wantCode: http.StatusBadRequest, + }, + { + name: "All Empty", + payload: dto.RegisterCandidateRequest{}, + wantCode: http.StatusBadRequest, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + body, _ := json.Marshal(tc.payload) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + coreHandlers.RegisterCandidate(rec, req) + + if rec.Code != tc.wantCode { + t.Errorf("Expected status %d, got %d", tc.wantCode, rec.Code) + } + }) + } +} + +func TestLoginHandler_InvalidPayload(t *testing.T) { + coreHandlers := createTestCoreHandlers(t) + if coreHandlers == nil { + t.Skip("Cannot create test handlers - skipping") + return + } + + body := bytes.NewBufferString("{invalid}") + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", body) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + coreHandlers.Login(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Errorf("Expected status %d, got %d", http.StatusBadRequest, rec.Code) + } +} + +// createTestCoreHandlers creates handlers with nil usecases for basic validation tests +// For full integration tests, wire up real mock implementations +func createTestCoreHandlers(t *testing.T) *handlers.CoreHandlers { + t.Helper() + + // Return nil - these tests need proper DI which we skip for now + // The real tests are in usecases/auth package + return handlers.NewCoreHandlers( + (*auth.LoginUseCase)(nil), + (*auth.RegisterCandidateUseCase)(nil), + (*tenant.CreateCompanyUseCase)(nil), + (*user.CreateUserUseCase)(nil), + (*user.ListUsersUseCase)(nil), + (*user.DeleteUserUseCase)(nil), + (*tenant.ListCompaniesUseCase)(nil), + nil, // auditService + ) +} diff --git a/backend/internal/core/usecases/auth/register_candidate.go b/backend/internal/core/usecases/auth/register_candidate.go index 2f6901e..c2226f9 100644 --- a/backend/internal/core/usecases/auth/register_candidate.go +++ b/backend/internal/core/usecases/auth/register_candidate.go @@ -3,6 +3,7 @@ package auth import ( "context" "errors" + "fmt" "github.com/google/uuid" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" @@ -12,12 +13,14 @@ import ( type RegisterCandidateUseCase struct { userRepo ports.UserRepository + companyRepo ports.CompanyRepository authService ports.AuthService } -func NewRegisterCandidateUseCase(uRepo ports.UserRepository, auth ports.AuthService) *RegisterCandidateUseCase { +func NewRegisterCandidateUseCase(uRepo ports.UserRepository, cRepo ports.CompanyRepository, auth ports.AuthService) *RegisterCandidateUseCase { return &RegisterCandidateUseCase{ userRepo: uRepo, + companyRepo: cRepo, authService: auth, } } @@ -35,10 +38,21 @@ func (uc *RegisterCandidateUseCase) Execute(ctx context.Context, input dto.Regis return nil, err } - // 3. Create Entity - // Candidates belong to their own tenant/workspace in this model logic + // 3. Create Candidate's Personal Tenant/Workspace candidateTenantID := uuid.New().String() + candidateCompany := entity.NewCompany( + candidateTenantID, + fmt.Sprintf("Candidate - %s", input.Name), + nil, // No document for candidates + nil, // No contact - will use user's contact info + ) + _, err = uc.companyRepo.Save(ctx, candidateCompany) + if err != nil { + return nil, fmt.Errorf("failed to create candidate workspace: %w", err) + } + + // 4. Create User Entity user := entity.NewUser(uuid.New().String(), candidateTenantID, input.Name, input.Email) user.PasswordHash = hashed diff --git a/backend/internal/core/usecases/auth/register_candidate_test.go b/backend/internal/core/usecases/auth/register_candidate_test.go index 7c71498..dd95582 100644 --- a/backend/internal/core/usecases/auth/register_candidate_test.go +++ b/backend/internal/core/usecases/auth/register_candidate_test.go @@ -39,6 +39,27 @@ func (m *MockUserRepo) Update(ctx context.Context, user *entity.User) (*entity.U } func (m *MockUserRepo) Delete(ctx context.Context, id string) error { return nil } +type MockCompanyRepo struct { + SaveFunc func(ctx context.Context, company *entity.Company) (*entity.Company, error) +} + +func (m *MockCompanyRepo) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) { + if m.SaveFunc != nil { + return m.SaveFunc(ctx, company) + } + return company, nil +} +func (m *MockCompanyRepo) FindByID(ctx context.Context, id string) (*entity.Company, error) { + return nil, nil +} +func (m *MockCompanyRepo) FindAll(ctx context.Context) ([]*entity.Company, error) { + return nil, nil +} +func (m *MockCompanyRepo) Update(ctx context.Context, company *entity.Company) (*entity.Company, error) { + return nil, nil +} +func (m *MockCompanyRepo) Delete(ctx context.Context, id string) error { return nil } + type MockAuthService struct { HashPasswordFunc func(password string) (string, error) GenerateTokenFunc func(userID, tenantID string, roles []string) (string, error) @@ -65,7 +86,7 @@ func (m *MockAuthService) ValidateToken(token string) (map[string]interface{}, e func TestRegisterCandidateUseCase_Execute(t *testing.T) { t.Run("Success", func(t *testing.T) { - repo := &MockUserRepo{ + userRepo := &MockUserRepo{ FindByEmailFunc: func(ctx context.Context, email string) (*entity.User, error) { return nil, nil // Email not found }, @@ -74,9 +95,10 @@ func TestRegisterCandidateUseCase_Execute(t *testing.T) { return user, nil }, } + companyRepo := &MockCompanyRepo{} authSvc := &MockAuthService{} - uc := auth.NewRegisterCandidateUseCase(repo, authSvc) + uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc) input := dto.RegisterCandidateRequest{ Name: "John Doe", @@ -103,13 +125,14 @@ func TestRegisterCandidateUseCase_Execute(t *testing.T) { }) t.Run("EmailAlreadyExists", func(t *testing.T) { - repo := &MockUserRepo{ + userRepo := &MockUserRepo{ FindByEmailFunc: func(ctx context.Context, email string) (*entity.User, error) { return &entity.User{ID: "existing"}, nil // Found }, } + companyRepo := &MockCompanyRepo{} authSvc := &MockAuthService{} - uc := auth.NewRegisterCandidateUseCase(repo, authSvc) + uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc) _, err := uc.Execute(context.Background(), dto.RegisterCandidateRequest{Email: "exists@example.com", Password: "123"}) @@ -124,14 +147,15 @@ func TestRegisterCandidateUseCase_Execute(t *testing.T) { t.Run("MetadataSaved", func(t *testing.T) { // Verify if username/phone ends up in metadata var capturedUser *entity.User - repo := &MockUserRepo{ + userRepo := &MockUserRepo{ SaveFunc: func(ctx context.Context, user *entity.User) (*entity.User, error) { capturedUser = user return user, nil }, } + companyRepo := &MockCompanyRepo{} authSvc := &MockAuthService{} - uc := auth.NewRegisterCandidateUseCase(repo, authSvc) + uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc) uc.Execute(context.Background(), dto.RegisterCandidateRequest{Username: "coder", Phone: "999"}) @@ -145,4 +169,26 @@ func TestRegisterCandidateUseCase_Execute(t *testing.T) { t.Errorf("Expected metadata phone '999', got %v", capturedUser.Metadata["phone"]) } }) + + t.Run("CompanyCreatedForCandidate", func(t *testing.T) { + var capturedCompany *entity.Company + userRepo := &MockUserRepo{} + companyRepo := &MockCompanyRepo{ + SaveFunc: func(ctx context.Context, company *entity.Company) (*entity.Company, error) { + capturedCompany = company + return company, nil + }, + } + authSvc := &MockAuthService{} + uc := auth.NewRegisterCandidateUseCase(userRepo, companyRepo, authSvc) + + uc.Execute(context.Background(), dto.RegisterCandidateRequest{Name: "Test User", Email: "test@test.com", Password: "123"}) + + if capturedCompany == nil { + t.Fatal("Company not created") + } + if capturedCompany.Name != "Candidate - Test User" { + t.Errorf("Expected company name 'Candidate - Test User', got %s", capturedCompany.Name) + } + }) } diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 2dfa718..a251de2 100755 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -50,7 +50,7 @@ func NewRouter() http.Handler { // UseCases loginUC := authUC.NewLoginUseCase(userRepo, authService) - registerCandidateUC := authUC.NewRegisterCandidateUseCase(userRepo, authService) + registerCandidateUC := authUC.NewRegisterCandidateUseCase(userRepo, companyRepo, authService) createCompanyUC := tenantUC.NewCreateCompanyUseCase(companyRepo, userRepo, authService) listCompaniesUC := tenantUC.NewListCompaniesUseCase(companyRepo) createUserUC := userUC.NewCreateUserUseCase(userRepo, authService) diff --git a/frontend/src/app/jobs/[id]/page.tsx b/frontend/src/app/jobs/[id]/page.tsx index 6fbcfc8..d024c94 100644 --- a/frontend/src/app/jobs/[id]/page.tsx +++ b/frontend/src/app/jobs/[id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { use } from "react"; +import { use, useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { Navbar } from "@/components/navbar"; import { Footer } from "@/components/footer"; @@ -15,7 +15,7 @@ import { import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Separator } from "@/components/ui/separator"; -import { mockJobs } from "@/lib/mock-data"; +import { jobsApi, transformJob, type Job } from "@/lib/api"; import { MapPin, Briefcase, @@ -31,9 +31,9 @@ import { Bookmark, Star, Globe, + Loader2, } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; import { motion } from "framer-motion"; @@ -48,8 +48,44 @@ export default function JobDetailPage({ const router = useRouter(); const [isFavorited, setIsFavorited] = useState(false); const [isBookmarked, setIsBookmarked] = useState(false); + const [job, setJob] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const job = mockJobs.find((j) => j.id === id); + useEffect(() => { + async function fetchJob() { + try { + setLoading(true); + const response = await jobsApi.getById(id); + if (response.data) { + setJob(transformJob(response.data)); + } else { + setError("Job not found"); + } + } catch (err) { + console.error("Error fetching job:", err); + setError("Failed to load job"); + } finally { + setLoading(false); + } + } + fetchJob(); + }, [id]); + + if (loading) { + return ( +
+ +
+
+ +

Loading job...

+
+
+
+
+ ); + } if (!job) { return ( @@ -189,8 +225,8 @@ export default function JobDetailPage({ > @@ -220,8 +256,8 @@ export default function JobDetailPage({ > {isFavorited ? "Favorited" : "Favorite"} @@ -486,28 +522,9 @@ export default function JobDetailPage({ Similar jobs - {mockJobs - .filter((j) => j.id !== job.id) - .slice(0, 3) - .map((similarJob) => ( - -
-

- {similarJob.title} -

-

- {similarJob.company} -

-
- - {similarJob.location} -
-
- - ))} +

+ Find more opportunities like this one. +