diff --git a/backend/internal/core/usecases/tenant/company_usecases_test.go b/backend/internal/core/usecases/tenant/company_usecases_test.go new file mode 100644 index 0000000..b0f6906 --- /dev/null +++ b/backend/internal/core/usecases/tenant/company_usecases_test.go @@ -0,0 +1,183 @@ +package tenant + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" + "github.com/rede5/gohorsejobs/backend/internal/core/dto" +) + +type fakeCompanyRepo struct { + saveFn func(context.Context, *entity.Company) (*entity.Company, error) + findAll []*entity.Company + findErr error +} + +func (f *fakeCompanyRepo) Save(ctx context.Context, company *entity.Company) (*entity.Company, error) { + if f.saveFn != nil { + return f.saveFn(ctx, company) + } + return company, nil +} +func (f *fakeCompanyRepo) FindByID(ctx context.Context, id string) (*entity.Company, error) { return nil, nil } +func (f *fakeCompanyRepo) FindAll(ctx context.Context) ([]*entity.Company, error) { + return f.findAll, f.findErr +} +func (f *fakeCompanyRepo) Update(ctx context.Context, company *entity.Company) (*entity.Company, error) { + return company, nil +} +func (f *fakeCompanyRepo) Delete(ctx context.Context, id string) error { return nil } + +type fakeUserRepoTenant struct { + findByEmailUser *entity.User + findByEmailErr error + saveErr error + savedUser *entity.User +} + +func (f *fakeUserRepoTenant) Save(ctx context.Context, user *entity.User) (*entity.User, error) { + f.savedUser = user + return user, f.saveErr +} +func (f *fakeUserRepoTenant) FindByID(ctx context.Context, id string) (*entity.User, error) { return nil, nil } +func (f *fakeUserRepoTenant) FindByEmail(ctx context.Context, email string) (*entity.User, error) { + return f.findByEmailUser, f.findByEmailErr +} +func (f *fakeUserRepoTenant) FindAllByTenant(ctx context.Context, tenantID string, limit, offset int) ([]*entity.User, int, error) { + return nil, 0, nil +} +func (f *fakeUserRepoTenant) LinkGuestApplications(ctx context.Context, email string, userID string) error { + return nil +} +func (f *fakeUserRepoTenant) Update(ctx context.Context, user *entity.User) (*entity.User, error) { return user, nil } +func (f *fakeUserRepoTenant) Delete(ctx context.Context, id string) error { return nil } + +type fakeAuthTenant struct { + hashResult string + hashErr error + token string + tokenErr error +} + +func (f *fakeAuthTenant) HashPassword(password string) (string, error) { return f.hashResult, f.hashErr } +func (f *fakeAuthTenant) VerifyPassword(hash, password string) bool { return false } +func (f *fakeAuthTenant) GenerateToken(userID, tenantID string, roles []string) (string, error) { + return f.token, f.tokenErr +} +func (f *fakeAuthTenant) ValidateToken(token string) (map[string]interface{}, error) { return nil, nil } + +func TestCreateCompanyUseCaseExecute_ValidationAndSuccess(t *testing.T) { + companyRepo := &fakeCompanyRepo{} + userRepo := &fakeUserRepoTenant{} + auth := &fakeAuthTenant{hashResult: "hashed", token: "jwt-token"} + uc := NewCreateCompanyUseCase(companyRepo, userRepo, auth) + + _, err := uc.Execute(context.Background(), dto.CreateCompanyRequest{}) + if err == nil { + t.Fatalf("expected company name validation error") + } + + invalidDoc := "123" + _, err = uc.Execute(context.Background(), dto.CreateCompanyRequest{Name: "ACME", Document: invalidDoc}) + if err == nil { + t.Fatalf("expected invalid document error") + } + + userRepo.findByEmailUser = entity.NewUser("u1", "t1", "Admin", "admin@acme.com") + _, err = uc.Execute(context.Background(), dto.CreateCompanyRequest{Name: "ACME", AdminEmail: "admin@acme.com"}) + if err == nil { + t.Fatalf("expected duplicate admin user error") + } + + userRepo.findByEmailUser = nil + companyRepo.saveFn = func(_ context.Context, company *entity.Company) (*entity.Company, error) { + if company.Name != "Acme Co" { + t.Fatalf("expected sanitized company name, got %q", company.Name) + } + company.ID = "company-1" + company.CreatedAt = time.Now() + return company, nil + } + birthDate := "1990-05-20" + website := "https://acme.test" + address := "Rua 1" + resp, err := uc.Execute(context.Background(), dto.CreateCompanyRequest{ + CompanyName: " Acme Co ", + Email: "contact@acme.test", + AdminEmail: "", + Password: "", + Website: &website, + Address: &address, + BirthDate: &birthDate, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.ID != "company-1" || resp.Token != "jwt-token" { + t.Fatalf("unexpected response: %+v", resp) + } + if userRepo.savedUser == nil || userRepo.savedUser.PasswordHash != "hashed" { + t.Fatalf("expected saved admin user with hashed password") + } + if userRepo.savedUser.BirthDate == nil { + t.Fatalf("expected birth date to be parsed") + } + if len(userRepo.savedUser.Roles) != 1 || userRepo.savedUser.Roles[0].Name != entity.RoleAdmin { + t.Fatalf("expected admin role") + } +} + +func TestCreateCompanyUseCaseExecute_RepositoryFailures(t *testing.T) { + companyRepo := &fakeCompanyRepo{} + userRepo := &fakeUserRepoTenant{} + auth := &fakeAuthTenant{hashResult: "hashed", tokenErr: errors.New("token fail")} + uc := NewCreateCompanyUseCase(companyRepo, userRepo, auth) + + companyRepo.saveFn = func(_ context.Context, company *entity.Company) (*entity.Company, error) { + return nil, errors.New("save company fail") + } + _, err := uc.Execute(context.Background(), dto.CreateCompanyRequest{Name: "ACME", AdminEmail: "admin@acme.com"}) + if err == nil { + t.Fatalf("expected save company error") + } + + companyRepo.saveFn = func(_ context.Context, company *entity.Company) (*entity.Company, error) { + company.ID = "company-2" + return company, nil + } + userRepo.saveErr = errors.New("save admin fail") + _, err = uc.Execute(context.Background(), dto.CreateCompanyRequest{Name: "ACME", AdminEmail: "admin@acme.com"}) + if err == nil { + t.Fatalf("expected save admin error") + } + + userRepo.saveErr = nil + resp, err := uc.Execute(context.Background(), dto.CreateCompanyRequest{Name: "ACME", AdminEmail: "admin@acme.com"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Token != "" { + t.Fatalf("expected empty token when generation fails") + } +} + +func TestListCompaniesUseCaseExecute(t *testing.T) { + repo := &fakeCompanyRepo{findAll: []*entity.Company{{ID: "c1", Name: "Acme", Status: "ACTIVE"}}} + uc := NewListCompaniesUseCase(repo) + resp, err := uc.Execute(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp) != 1 || resp[0].ID != "c1" { + t.Fatalf("unexpected response: %+v", resp) + } + + repo.findErr = errors.New("db fail") + _, err = uc.Execute(context.Background()) + if err == nil { + t.Fatalf("expected find all error") + } +} diff --git a/backend/internal/core/usecases/user/user_usecases_test.go b/backend/internal/core/usecases/user/user_usecases_test.go new file mode 100644 index 0000000..9e11df3 --- /dev/null +++ b/backend/internal/core/usecases/user/user_usecases_test.go @@ -0,0 +1,275 @@ +package user + +import ( + "context" + "errors" + "testing" + + "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" + "github.com/rede5/gohorsejobs/backend/internal/core/dto" +) + +type fakeUserRepo struct { + findByEmailUser *entity.User + findByEmailErr error + saveFn func(context.Context, *entity.User) (*entity.User, error) + findByIDUser *entity.User + findByIDErr error + findAllByTenantUsers []*entity.User + findAllByTenantTotal int + findAllByTenantErr error + updateFn func(context.Context, *entity.User) (*entity.User, error) + deleteErr error + capturedLimit int + capturedOffset int +} + +func (f *fakeUserRepo) Save(ctx context.Context, user *entity.User) (*entity.User, error) { + if f.saveFn != nil { + return f.saveFn(ctx, user) + } + return user, nil +} + +func (f *fakeUserRepo) FindByID(ctx context.Context, id string) (*entity.User, error) { + return f.findByIDUser, f.findByIDErr +} + +func (f *fakeUserRepo) FindByEmail(ctx context.Context, email string) (*entity.User, error) { + return f.findByEmailUser, f.findByEmailErr +} + +func (f *fakeUserRepo) FindAllByTenant(ctx context.Context, tenantID string, limit, offset int) ([]*entity.User, int, error) { + f.capturedLimit = limit + f.capturedOffset = offset + return f.findAllByTenantUsers, f.findAllByTenantTotal, f.findAllByTenantErr +} + +func (f *fakeUserRepo) LinkGuestApplications(ctx context.Context, email string, userID string) error { + return nil +} + +func (f *fakeUserRepo) Update(ctx context.Context, user *entity.User) (*entity.User, error) { + if f.updateFn != nil { + return f.updateFn(ctx, user) + } + return user, nil +} + +func (f *fakeUserRepo) Delete(ctx context.Context, id string) error { + return f.deleteErr +} + +type fakeAuthService struct { + hashPasswordResult string + hashPasswordErr error +} + +func (f *fakeAuthService) HashPassword(password string) (string, error) { + return f.hashPasswordResult, f.hashPasswordErr +} + +func (f *fakeAuthService) VerifyPassword(hash, password string) bool { + return false +} + +func (f *fakeAuthService) GenerateToken(userID, tenantID string, roles []string) (string, error) { + return "", nil +} + +func (f *fakeAuthService) ValidateToken(token string) (map[string]interface{}, error) { + return nil, nil +} + +func TestCreateUserUseCaseExecute_ValidationAndSuccess(t *testing.T) { + repo := &fakeUserRepo{} + auth := &fakeAuthService{hashPasswordResult: "hashed"} + uc := NewCreateUserUseCase(repo, auth) + + _, err := uc.Execute(context.Background(), dto.CreateUserRequest{Email: "x"}, "tenant-1") + if err == nil { + t.Fatalf("expected invalid email error") + } + + repo.findByEmailUser = entity.NewUser("u-existing", "tenant-1", "A", "john@example.com") + _, err = uc.Execute(context.Background(), dto.CreateUserRequest{ + Name: "John", + Email: "john@example.com", + Password: "secret", + }, "tenant-1") + if err == nil { + t.Fatalf("expected duplicate email error") + } + + repo.findByEmailUser = nil + auth.hashPasswordErr = errors.New("hash failed") + _, err = uc.Execute(context.Background(), dto.CreateUserRequest{ + Name: "John", + Email: "john@example.com", + Password: "secret", + }, "tenant-1") + if err == nil { + t.Fatalf("expected hash error") + } + + auth.hashPasswordErr = nil + repo.saveFn = func(_ context.Context, user *entity.User) (*entity.User, error) { + if user.TenantID != "tenant-2" { + t.Fatalf("expected tenant from input when current tenant empty") + } + if user.PasswordHash != "hashed" { + t.Fatalf("expected hashed password") + } + if len(user.Roles) != 1 || user.Roles[0].Name != entity.RoleAdmin { + t.Fatalf("expected admin role") + } + return user, nil + } + status := "inactive" + tenantID := "tenant-2" + resp, err := uc.Execute(context.Background(), dto.CreateUserRequest{ + Name: " Jo ", + Email: " JOHN@example.com ", + Password: "secret", + Roles: []string{entity.RoleAdmin}, + Status: &status, + TenantID: &tenantID, + }, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Status != status { + t.Fatalf("expected status %s, got %s", status, resp.Status) + } +} + +func TestListUsersUseCaseExecute_NormalizesPaginationAndMapsRoles(t *testing.T) { + repo := &fakeUserRepo{ + findAllByTenantUsers: []*entity.User{{ + ID: "u1", Name: "Ana", Email: "ana@example.com", Status: "active", Roles: []entity.Role{{Name: entity.RoleRecruiter}}, + }}, + findAllByTenantTotal: 1, + } + uc := NewListUsersUseCase(repo) + resp, err := uc.Execute(context.Background(), "tenant", -5, 999) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if repo.capturedLimit != 100 || repo.capturedOffset != 0 { + t.Fatalf("expected normalized limit/offset to 100/0, got %d/%d", repo.capturedLimit, repo.capturedOffset) + } + if resp.Pagination.Page != 1 || resp.Pagination.Limit != 100 || resp.Pagination.Total != 1 { + t.Fatalf("unexpected pagination: %+v", resp.Pagination) + } + users, ok := resp.Data.([]dto.UserResponse) + if !ok || len(users) != 1 || len(users[0].Roles) != 1 || users[0].Roles[0] != entity.RoleRecruiter { + t.Fatalf("unexpected users payload: %#v", resp.Data) + } + + repo.findAllByTenantErr = errors.New("db down") + _, err = uc.Execute(context.Background(), "tenant", 1, 10) + if err == nil { + t.Fatalf("expected repository error") + } +} + +func TestUpdateUserUseCaseExecute_Flows(t *testing.T) { + repo := &fakeUserRepo{} + uc := NewUpdateUserUseCase(repo) + + repo.findByIDErr = errors.New("find error") + _, err := uc.Execute(context.Background(), "u1", "t1", dto.UpdateUserRequest{}) + if err == nil { + t.Fatalf("expected find error") + } + + repo.findByIDErr = nil + repo.findByIDUser = nil + _, err = uc.Execute(context.Background(), "u1", "t1", dto.UpdateUserRequest{}) + if err == nil { + t.Fatalf("expected not found error") + } + + repo.findByIDUser = &entity.User{ID: "u1", TenantID: "other"} + _, err = uc.Execute(context.Background(), "u1", "t1", dto.UpdateUserRequest{}) + if err == nil { + t.Fatalf("expected forbidden error") + } + + name := "New Name" + email := "new@example.com" + active := false + phone := "11999990000" + bio := "bio" + avatar := "https://avatar" + roles := []string{entity.RoleAdmin, entity.RoleRecruiter} + skills := []string{"go", "sql"} + repo.findByIDUser = &entity.User{ID: "u1", TenantID: "t1", Status: "active"} + repo.updateFn = func(_ context.Context, user *entity.User) (*entity.User, error) { + if user.Status != "inactive" || user.Name != name || user.Email != email || user.AvatarUrl != avatar { + t.Fatalf("user not updated as expected: %+v", user) + } + if len(user.Roles) != 2 || user.Roles[0].Name != entity.RoleAdmin { + t.Fatalf("roles not updated: %+v", user.Roles) + } + if len(user.Skills) != 2 { + t.Fatalf("skills not updated") + } + return user, nil + } + resp, err := uc.Execute(context.Background(), "u1", "t1", dto.UpdateUserRequest{ + Name: &name, + Email: &email, + Phone: &phone, + Bio: &bio, + Active: &active, + Roles: &roles, + AvatarUrl: &avatar, + Skills: skills, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Status != "inactive" { + t.Fatalf("expected inactive status") + } + + status := "active" + repo.updateFn = func(_ context.Context, user *entity.User) (*entity.User, error) { + if user.Status != "active" { + t.Fatalf("expected explicit status to override active") + } + return nil, errors.New("update failed") + } + _, err = uc.Execute(context.Background(), "u1", "", dto.UpdateUserRequest{Status: &status}) + if err == nil { + t.Fatalf("expected update error") + } +} + +func TestDeleteUserUseCaseExecute_Flows(t *testing.T) { + repo := &fakeUserRepo{} + uc := NewDeleteUserUseCase(repo) + + repo.findByIDErr = errors.New("boom") + if err := uc.Execute(context.Background(), "u1", "t1"); err == nil { + t.Fatalf("expected find error") + } + + repo.findByIDErr = nil + repo.findByIDUser = &entity.User{ID: "u1", TenantID: "other"} + if err := uc.Execute(context.Background(), "u1", "t1"); err == nil { + t.Fatalf("expected tenant mismatch error") + } + + repo.findByIDUser = &entity.User{ID: "u1", TenantID: "t1"} + repo.deleteErr = errors.New("delete fail") + if err := uc.Execute(context.Background(), "u1", "t1"); err == nil { + t.Fatalf("expected delete error") + } + + repo.deleteErr = nil + if err := uc.Execute(context.Background(), "u1", ""); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/docs/txt/backend_coverage_modes.md b/docs/txt/backend_coverage_modes.md new file mode 100644 index 0000000..cb6a477 --- /dev/null +++ b/docs/txt/backend_coverage_modes.md @@ -0,0 +1,12 @@ +# Backend Coverage (com e sem banco) + +## Sem banco de dados (unitário) +- **Cobertura total:** **27.2%** +- **Fonte:** `docs/txt/backend_coverage.txt` (`total: (statements) 27.2%`). + +## Com banco de dados (integração) +- **Cobertura total reportada:** **~59.8%** +- **Fonte:** `docs/TEST_REPORT.md` (seção de backend com menção de cobertura total de ~59.8% e integração 100% pass). + +## Observação +Esses números vêm de artefatos já versionados no repositório e representam execuções em momentos distintos.