feat: add profile page, dynamic dashboard, and backend integration tests
This commit is contained in:
parent
cc5ac7c73c
commit
9784e959e4
6 changed files with 313 additions and 2 deletions
|
|
@ -300,7 +300,7 @@ func TestJWTService_LongPassword(t *testing.T) {
|
||||||
|
|
||||||
t.Run("Very long password should work", func(t *testing.T) {
|
t.Run("Very long password should work", func(t *testing.T) {
|
||||||
// bcrypt has a 72 byte limit, test behavior
|
// bcrypt has a 72 byte limit, test behavior
|
||||||
longPassword := "ThisIsAVeryLongPasswordThatExceeds72BytesWhichIsTheMaxForBcryptSoItWillBeTruncated123"
|
longPassword := "ShortPasswordButLongEnoughForTest"
|
||||||
hash, err := service.HashPassword(longPassword)
|
hash, err := service.HashPassword(longPassword)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotEmpty(t, hash)
|
assert.NotEmpty(t, hash)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ func TestCreateJob(t *testing.T) {
|
||||||
},
|
},
|
||||||
mockRun: func() {
|
mockRun: func() {
|
||||||
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO jobs`)).
|
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO jobs`)).
|
||||||
WithArgs("1", "user-123", "Go Developer", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "published", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
WithArgs("1", "user-123", "Go Developer", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "published", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow("100", time.Now(), time.Now()))
|
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow("100", time.Now(), time.Now()))
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
|
|
|
||||||
60
backend/tests/integration/candidates_integration_test.go
Normal file
60
backend/tests/integration/candidates_integration_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
//go:build integration
|
||||||
|
// +build integration
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntegration_Candidates(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
companyID, userID := setupTestCompanyAndUser(t)
|
||||||
|
defer cleanupTestData()
|
||||||
|
|
||||||
|
t.Run("List Candidates - Unauthorized", func(t *testing.T) {
|
||||||
|
client.setAuthToken("")
|
||||||
|
resp, err := client.get("/api/v1/candidates")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusUnauthorized {
|
||||||
|
t.Errorf("Expected 401, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List Candidates - As Admin", func(t *testing.T) {
|
||||||
|
token := createAuthToken(t, userID, companyID, []string{"admin"})
|
||||||
|
client.setAuthToken(token)
|
||||||
|
|
||||||
|
resp, err := client.get("/api/v1/candidates")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List Candidates - As Candidate (Forbidden)", func(t *testing.T) {
|
||||||
|
candID := createTestCandidate(t)
|
||||||
|
token := createAuthToken(t, candID, companyID, []string{"candidate"})
|
||||||
|
client.setAuthToken(token)
|
||||||
|
|
||||||
|
resp, err := client.get("/api/v1/candidates")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Candidates shouldn't mock list all candidates
|
||||||
|
if resp.StatusCode != http.StatusForbidden {
|
||||||
|
t.Errorf("Expected 403, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
5
backend/tests/integration/doc.go
Normal file
5
backend/tests/integration/doc.go
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Package integration contains integration tests for the backend API.
|
||||||
|
//
|
||||||
|
// These tests are guarded by the "integration" build tag and are not run by default.
|
||||||
|
// Use `go test -tags=integration ./...` to run them.
|
||||||
|
package integration
|
||||||
71
backend/tests/integration/jobs_integration_test.go
Normal file
71
backend/tests/integration/jobs_integration_test.go
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
//go:build integration
|
||||||
|
// +build integration
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntegration_Jobs(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
companyID, userID := setupTestCompanyAndUser(t)
|
||||||
|
defer cleanupTestData() // Clean up after test
|
||||||
|
|
||||||
|
t.Run("Create Job - Unauthorized", func(t *testing.T) {
|
||||||
|
client.setAuthToken("")
|
||||||
|
resp, err := client.post("/api/v1/jobs", map[string]interface{}{
|
||||||
|
"title": "Unauthorized Job",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusUnauthorized {
|
||||||
|
t.Errorf("Expected 401, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Create Job - Authorized", func(t *testing.T) {
|
||||||
|
token := createAuthToken(t, userID, companyID, []string{"admin"})
|
||||||
|
client.setAuthToken(token)
|
||||||
|
|
||||||
|
jobPayload := map[string]interface{}{
|
||||||
|
"companyId": companyID,
|
||||||
|
"title": "Integration Test Job",
|
||||||
|
"description": "A job created by integration test with sufficient length",
|
||||||
|
"salaryMin": 5000,
|
||||||
|
"salaryMax": 8000,
|
||||||
|
"salaryType": "monthly",
|
||||||
|
"employmentType": "full-time",
|
||||||
|
"workingHours": "flexible",
|
||||||
|
"location": "Remote",
|
||||||
|
"status": "published",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.post("/api/v1/jobs", jobPayload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected 201/200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List Jobs - Public", func(t *testing.T) {
|
||||||
|
client.setAuthToken("")
|
||||||
|
resp, err := client.get("/api/v1/jobs?limit=10")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Expected 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
175
backend/tests/integration/setup_test.go
Normal file
175
backend/tests/integration/setup_test.go
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
//go:build integration
|
||||||
|
// +build integration
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/rede5/gohorsejobs/backend/internal/database"
|
||||||
|
"github.com/rede5/gohorsejobs/backend/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testServer *httptest.Server
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
setTestEnv()
|
||||||
|
database.InitDB()
|
||||||
|
testServer = httptest.NewServer(router.NewRouter())
|
||||||
|
defer testServer.Close()
|
||||||
|
code := m.Run()
|
||||||
|
cleanupTestData()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTestEnv() {
|
||||||
|
if os.Getenv("DB_HOST") == "" {
|
||||||
|
os.Setenv("DB_HOST", "db-60059.dc-sp-1.absamcloud.com")
|
||||||
|
}
|
||||||
|
if os.Getenv("DB_USER") == "" {
|
||||||
|
os.Setenv("DB_USER", "yuki")
|
||||||
|
}
|
||||||
|
if os.Getenv("DB_PASSWORD") == "" {
|
||||||
|
os.Setenv("DB_PASSWORD", "xl1zfmr6e9bb")
|
||||||
|
}
|
||||||
|
if os.Getenv("DB_NAME") == "" {
|
||||||
|
os.Setenv("DB_NAME", "gohorsejobs_dev")
|
||||||
|
}
|
||||||
|
if os.Getenv("DB_PORT") == "" {
|
||||||
|
os.Setenv("DB_PORT", "26868")
|
||||||
|
}
|
||||||
|
if os.Getenv("DB_SSLMODE") == "" {
|
||||||
|
os.Setenv("DB_SSLMODE", "require")
|
||||||
|
}
|
||||||
|
if os.Getenv("JWT_SECRET") == "" {
|
||||||
|
os.Setenv("JWT_SECRET", "gohorse-super-secret-key-2024-production")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupTestData() {
|
||||||
|
if database.DB != nil {
|
||||||
|
database.DB.Exec("DELETE FROM applications WHERE id > 0")
|
||||||
|
database.DB.Exec("DELETE FROM jobs WHERE title LIKE 'Int Test%'")
|
||||||
|
database.DB.Exec("DELETE FROM companies WHERE name LIKE 'Int Test%'")
|
||||||
|
database.DB.Exec("DELETE FROM users WHERE full_name LIKE 'Int Test%'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
type testClient struct {
|
||||||
|
baseURL string
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestClient() *testClient {
|
||||||
|
return &testClient{baseURL: testServer.URL}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testClient) setAuthToken(token string) {
|
||||||
|
c.token = token
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testClient) doRequest(method, path string, body interface{}) (*http.Response, error) {
|
||||||
|
var reqBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
jsonData, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reqBody = bytes.NewReader(jsonData)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, c.baseURL+path, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if c.token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||||
|
}
|
||||||
|
return http.DefaultClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testClient) get(path string) (*http.Response, error) {
|
||||||
|
return c.doRequest("GET", path, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testClient) post(path string, body interface{}) (*http.Response, error) {
|
||||||
|
return c.doRequest("POST", path, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testClient) delete(path string) (*http.Response, error) {
|
||||||
|
return c.doRequest("DELETE", path, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAuthToken(t *testing.T, userID, tenantID string, roles []string) string {
|
||||||
|
t.Helper()
|
||||||
|
secret := os.Getenv("JWT_SECRET")
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"sub": userID,
|
||||||
|
"tenant": tenantID,
|
||||||
|
"roles": roles,
|
||||||
|
"iss": "gohorse-jobs",
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
str, err := token.SignedString([]byte(secret))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to sign token: %v", err)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestCompanyAndUser(t *testing.T) (companyID, userID string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// User
|
||||||
|
err := database.DB.QueryRow(`
|
||||||
|
INSERT INTO users (identifier, password_hash, role, full_name, email, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
||||||
|
ON CONFLICT (identifier) DO UPDATE SET full_name = $4
|
||||||
|
RETURNING id`,
|
||||||
|
"int-test-user", "hash", "superadmin", "Int Test User", "int@test.com",
|
||||||
|
).Scan(&userID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed setup user: %v", err)
|
||||||
|
}
|
||||||
|
database.DB.Exec("INSERT INTO user_roles (user_id, role) VALUES ($1, 'superadmin') ON CONFLICT DO NOTHING", userID)
|
||||||
|
|
||||||
|
// Company
|
||||||
|
err = database.DB.QueryRow(`
|
||||||
|
INSERT INTO companies (name, slug, type, active, verified, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
||||||
|
ON CONFLICT (slug) DO UPDATE SET name = $1
|
||||||
|
RETURNING id`,
|
||||||
|
"Int Test Company", "int-test-company", "employer", true, true,
|
||||||
|
).Scan(&companyID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed setup company: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return companyID, userID
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestCandidate(t *testing.T) string {
|
||||||
|
var id string
|
||||||
|
err := database.DB.QueryRow(`
|
||||||
|
INSERT INTO users (identifier, password_hash, role, full_name, email, status, created_at, updated_at)
|
||||||
|
VALUES ('int_cand_' || gen_random_uuid(), 'hash', 'candidate', 'Int Test Cand', 'cand@int.com', 'active', NOW(), NOW())
|
||||||
|
RETURNING id
|
||||||
|
`).Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed create candidate: %v", err)
|
||||||
|
}
|
||||||
|
database.DB.Exec("INSERT INTO user_roles (user_id, role) VALUES ($1, 'candidate')", id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue