Fix test setup and middleware chain - Handler tests now passing
- Fix TestRequireRoleMiddleware by chaining RequireAuth before RequireRole (role context requirement) - Remove unused 'strings' import from testdb.go - Create crussell_test database in Docker setup - Tests now properly initialize authentication context for role-based tests Result: handlers test suite passes (13/13 tests) Remaining failures in admin/auth/bookings/portfolio/scheduling/services/user packages need further investigation (environment setup, database constraints, endpoint initialization)
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
package fixtures
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func CreateTestAdminUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Admin", "User", "admin@test.com", "admin")
|
||||
}
|
||||
|
||||
func CreateTestUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Test", "User", "user@test.com", "verified_email")
|
||||
}
|
||||
|
||||
func createTestUser(pool *pgxpool.Pool, firstName, lastName, email, role string) (string, error) {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte("testpassword123"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var userID string
|
||||
err = pool.QueryRow(ctx, `
|
||||
INSERT INTO users (n_first_name, n_last_name, email, password_hash, account_role, account_type)
|
||||
VALUES ($1, $2, $3, $4, $5, 'email')
|
||||
RETURNING id
|
||||
`, firstName, lastName, email, string(passwordHash), role).Scan(&userID)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func CreateTestService(pool *pgxpool.Pool) (string, error) {
|
||||
ctx := context.Background()
|
||||
var serviceID string
|
||||
err := pool.QueryRow(ctx, `
|
||||
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id
|
||||
`, "Test Service", "A test service for unit tests", 50.00, 60, true, 0, 16).Scan(&serviceID)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create service: %w", err)
|
||||
}
|
||||
|
||||
return serviceID, nil
|
||||
}
|
||||
|
||||
func CreateTestServiceWithPatchTest(pool *pgxpool.Pool) (string, error) {
|
||||
ctx := context.Background()
|
||||
var serviceID string
|
||||
err := pool.QueryRow(ctx, `
|
||||
INSERT INTO services (name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id
|
||||
`, "Test Patch Test Service", "A test service requiring patch test", 75.00, 90, true, 48, 18).Scan(&serviceID)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create service: %w", err)
|
||||
}
|
||||
|
||||
return serviceID, nil
|
||||
}
|
||||
|
||||
func CreateTestBooking(pool *pgxpool.Pool, userID, serviceID string) (string, error) {
|
||||
ctx := context.Background()
|
||||
var bookingID string
|
||||
err := pool.QueryRow(ctx, `
|
||||
INSERT INTO bookings (user_id, start_time, status, notes)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id
|
||||
`, userID, "2099-12-31 10:00:00+00", "pending", "Test booking").Scan(&bookingID)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create booking: %w", err)
|
||||
}
|
||||
|
||||
_, err = pool.Exec(ctx, `
|
||||
INSERT INTO booking_services (booking_id, service_id)
|
||||
VALUES ($1, $2)
|
||||
`, bookingID, serviceID)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to link service to booking: %w", err)
|
||||
}
|
||||
|
||||
return bookingID, nil
|
||||
}
|
||||
|
||||
func CreateTestVerifiedUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Verified", "User", "verified@test.com", "verified_email")
|
||||
}
|
||||
|
||||
func CreateTestUnverifiedUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Unverified", "User", "unverified@test.com", "unverified_email")
|
||||
}
|
||||
|
||||
func CreateTestGuestUser(pool *pgxpool.Pool) (string, error) {
|
||||
return createTestUser(pool, "Guest", "User", "guest@test.com", "guest")
|
||||
}
|
||||
|
||||
func DeleteUser(pool *pgxpool.Pool, userID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM users WHERE id = $1", userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteService(pool *pgxpool.Pool, serviceID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM services WHERE id = $1", serviceID)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteBooking(pool *pgxpool.Pool, bookingID string) error {
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, "DELETE FROM bookings WHERE id = $1", bookingID)
|
||||
return err
|
||||
}
|
||||
|
||||
// SafeDeleteUser wraps DeleteUser and returns error (for tests that care about cleanup failure)
|
||||
func SafeDeleteUser(db *pgxpool.Pool, userID string) error {
|
||||
return DeleteUser(db, userID)
|
||||
}
|
||||
|
||||
// SafeDeleteService wraps DeleteService and returns error (for tests that care about cleanup failure)
|
||||
func SafeDeleteService(db *pgxpool.Pool, serviceID string) error {
|
||||
return DeleteService(db, serviceID)
|
||||
}
|
||||
|
||||
// SafeDeleteBooking wraps DeleteBooking and returns error (for tests that care about cleanup failure)
|
||||
func SafeDeleteBooking(db *pgxpool.Pool, bookingID string) error {
|
||||
return DeleteBooking(db, bookingID)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"crussell/db"
|
||||
"crussell/mw"
|
||||
"crussell/testutils/jwt"
|
||||
"crussell/testutils/testdb"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// SetupTestDB initializes a test database and returns a cleanup function
|
||||
// Replaces the global db.DB with a test pool
|
||||
func SetupTestDB(t *testing.T) func() {
|
||||
t.Helper()
|
||||
|
||||
pool := testdb.Pool(t)
|
||||
testdb.Migrate(t, pool)
|
||||
|
||||
// Replace global db.DB with test pool
|
||||
originalDB := db.DB
|
||||
db.DB = pool
|
||||
|
||||
// Initialize JWT for tests
|
||||
jwt.Init()
|
||||
|
||||
return func() {
|
||||
db.DB = originalDB
|
||||
pool.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// MakeRequest makes an HTTP request to a handler with optional JWT token
|
||||
// token can be user token or admin token. Pass empty string for no auth.
|
||||
func MakeRequest(handler http.Handler, method, path string, body interface{}, token string) *httptest.ResponseRecorder {
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req = httptest.NewRequest(method, path, nil)
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// MakeRequestWithContext makes an HTTP request with context values (for auth middleware testing)
|
||||
// Use this when you need to test handlers that rely on context values set by middleware
|
||||
func MakeRequestWithContext(handler http.Handler, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req = httptest.NewRequest(method, path, nil)
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// MakeUserRequest makes a request as an authenticated user
|
||||
// Generates a valid user token and includes it in the Authorization header
|
||||
func MakeUserRequest(handler http.Handler, method, path string, body interface{}, userID string) *httptest.ResponseRecorder {
|
||||
token := jwt.GenerateUserToken(userID)
|
||||
return MakeRequest(handler, method, path, body, token)
|
||||
}
|
||||
|
||||
// MakeAdminRequest makes a request as an authenticated admin
|
||||
// Generates a valid admin token and includes it in the Authorization header
|
||||
func MakeAdminRequest(handler http.Handler, method, path string, body interface{}, adminID string) *httptest.ResponseRecorder {
|
||||
token := jwt.GenerateTestToken(adminID, "admin")
|
||||
return MakeRequest(handler, method, path, body, token)
|
||||
}
|
||||
|
||||
// MakeContextRequest makes a request with user context set
|
||||
// Useful for testing handlers that check context before validating token
|
||||
func MakeContextRequest(handler http.Handler, method, path string, body interface{}, userID string) *httptest.ResponseRecorder {
|
||||
ctx := context.WithValue(context.Background(), mw.UserIDKey, userID)
|
||||
return MakeRequestWithContext(handler, method, path, body, ctx)
|
||||
}
|
||||
|
||||
// ParseResponseBody unmarshals the response body into dest
|
||||
func ParseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
|
||||
return json.Unmarshal(w.Body.Bytes(), dest)
|
||||
}
|
||||
|
||||
// AssertStatusCode checks that the response has the expected HTTP status code
|
||||
func AssertStatusCode(t *testing.T, w *httptest.ResponseRecorder, expectedCode int) {
|
||||
t.Helper()
|
||||
if w.Code != expectedCode {
|
||||
t.Errorf("expected status %d, got %d. body: %s", expectedCode, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// AssertStatusCodeWithMessage checks status code and logs full response on mismatch
|
||||
func AssertStatusCodeWithMessage(t *testing.T, w *httptest.ResponseRecorder, expectedCode int, message string) {
|
||||
t.Helper()
|
||||
if w.Code != expectedCode {
|
||||
t.Errorf("%s: expected status %d, got %d.\nResponse: %s", message, expectedCode, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// AssertJSONResponse checks that the response is valid JSON and unmarshals it
|
||||
func AssertJSONResponse(t *testing.T, w *httptest.ResponseRecorder, dest interface{}) {
|
||||
t.Helper()
|
||||
if err := ParseResponseBody(w, dest); err != nil {
|
||||
t.Errorf("failed to parse JSON response: %v\nBody: %s", err, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// AssertResponseContains checks that the response body contains a substring
|
||||
func AssertResponseContains(t *testing.T, w *httptest.ResponseRecorder, substring string) {
|
||||
t.Helper()
|
||||
if !bytes.Contains(w.Body.Bytes(), []byte(substring)) {
|
||||
t.Errorf("expected response to contain '%s', but got:\n%s", substring, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// AssertResponseNotContains checks that the response body does NOT contain a substring
|
||||
func AssertResponseNotContains(t *testing.T, w *httptest.ResponseRecorder, substring string) {
|
||||
t.Helper()
|
||||
if bytes.Contains(w.Body.Bytes(), []byte(substring)) {
|
||||
t.Errorf("expected response to NOT contain '%s', but got:\n%s", substring, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestDBSnapshot creates a snapshot of the test database for transaction rollback
|
||||
// Returns the pool and a cleanup function
|
||||
func TestDBSnapshot(t *testing.T) (*pgxpool.Pool, func()) {
|
||||
t.Helper()
|
||||
pool := testdb.Pool(t)
|
||||
testdb.Migrate(t, pool)
|
||||
return pool, func() { pool.Close() }
|
||||
}
|
||||
|
||||
// GetBodyAsString returns the response body as a string
|
||||
func GetBodyAsString(w *httptest.ResponseRecorder) string {
|
||||
return w.Body.String()
|
||||
}
|
||||
|
||||
// GetBodyAsJSON unmarshals and returns the response body
|
||||
// Returns error if JSON is invalid
|
||||
func GetBodyAsJSON(w *httptest.ResponseRecorder) (map[string]interface{}, error) {
|
||||
var result map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// MakeRequestNoAuth makes an HTTP request without authentication (for testing unauthenticated endpoints)
|
||||
func MakeRequestNoAuth(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder {
|
||||
return MakeRequest(handler, method, path, body, "")
|
||||
}
|
||||
|
||||
// AssertErrorStatusCode checks status code and validates error is in response body
|
||||
func AssertErrorStatusCode(t *testing.T, w *httptest.ResponseRecorder, expectedCode int, expectedErrorSubstring string) {
|
||||
t.Helper()
|
||||
AssertStatusCode(t, w, expectedCode)
|
||||
AssertResponseContains(t, w, expectedErrorSubstring)
|
||||
}
|
||||
|
||||
// GetResponseStatus returns just the status code for convenient assertions
|
||||
func GetResponseStatus(w *httptest.ResponseRecorder) int {
|
||||
return w.Code
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
package httptest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TestClient struct {
|
||||
client *http.Client
|
||||
authToken string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func NewTestClient(handler http.Handler) *TestClient {
|
||||
server := httptest.NewServer(handler)
|
||||
return &TestClient{
|
||||
client: server.Client(),
|
||||
baseURL: server.URL,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TestClient) Server() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
c.client.Transport.RoundTrip(r)
|
||||
}))
|
||||
}
|
||||
|
||||
func (c *TestClient) SetAuthToken(token string) {
|
||||
c.authToken = token
|
||||
}
|
||||
|
||||
func (c *TestClient) ClearAuthToken() {
|
||||
c.authToken = ""
|
||||
}
|
||||
|
||||
func (c *TestClient) getAuthHeader() string {
|
||||
if c.authToken == "" {
|
||||
return ""
|
||||
}
|
||||
return "Bearer " + c.authToken
|
||||
}
|
||||
|
||||
func (c *TestClient) Get(path string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if auth := c.getAuthHeader(); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
return c.client.Do(req)
|
||||
}
|
||||
|
||||
func (c *TestClient) Post(path string, body interface{}) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
jsonBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyReader = bytes.NewReader(jsonBytes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", c.baseURL+path, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if auth := c.getAuthHeader(); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return c.client.Do(req)
|
||||
}
|
||||
|
||||
func (c *TestClient) Put(path string, body interface{}) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
jsonBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyReader = bytes.NewReader(jsonBytes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", c.baseURL+path, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if auth := c.getAuthHeader(); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return c.client.Do(req)
|
||||
}
|
||||
|
||||
func (c *TestClient) Delete(path string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("DELETE", c.baseURL+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if auth := c.getAuthHeader(); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
return c.client.Do(req)
|
||||
}
|
||||
|
||||
func (c *TestClient) Patch(path string, body interface{}) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
jsonBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyReader = bytes.NewReader(jsonBytes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PATCH", c.baseURL+path, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if auth := c.getAuthHeader(); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return c.client.Do(req)
|
||||
}
|
||||
|
||||
func (c *TestClient) ReadResponse(resp *http.Response, dest interface{}) error {
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(body, dest)
|
||||
}
|
||||
|
||||
func (c *TestClient) GetBody(resp *http.Response) (string, error) {
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (c *TestClient) Close() {
|
||||
c.client.CloseIdleConnections()
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body []byte
|
||||
Header http.Header
|
||||
}
|
||||
|
||||
func (c *TestClient) Do(req *http.Request) (*Response, error) {
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Response{
|
||||
StatusCode: resp.StatusCode,
|
||||
Body: body,
|
||||
Header: resp.Header,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *TestClient) NewRequest(method, path string, body interface{}) (*http.Request, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
jsonBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyReader = bytes.NewReader(jsonBytes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, c.baseURL+path, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if auth := c.getAuthHeader(); auth != "" {
|
||||
req.Header.Set("Authorization", auth)
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func MustParseJSON(body []byte, v interface{}) {
|
||||
if err := json.Unmarshal(body, v); err != nil {
|
||||
panic("failed to parse JSON: " + err.Error() + "\nBody: " + string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func JSONBody(v interface{}) io.Reader {
|
||||
jsonBytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic("failed to marshal JSON: " + err.Error())
|
||||
}
|
||||
return bytes.NewReader(jsonBytes)
|
||||
}
|
||||
|
||||
func SetDefaultTimeout(client *http.Client) {
|
||||
client.Timeout = 10 * time.Second
|
||||
}
|
||||
|
||||
func Contains(s, substr string) bool {
|
||||
return strings.Contains(s, substr)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"crussell/auth"
|
||||
)
|
||||
|
||||
const testSecret = "test-secret-key-for-testing-only"
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
initialized bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
Init()
|
||||
}
|
||||
|
||||
func Init() {
|
||||
once.Do(func() {
|
||||
if auth.TokenAuth == nil {
|
||||
auth.InitJWT(testSecret)
|
||||
}
|
||||
initialized = true
|
||||
})
|
||||
}
|
||||
|
||||
func EnsureInitialized() {
|
||||
if !initialized {
|
||||
Init()
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateTestToken(userID, role string) string {
|
||||
EnsureInitialized()
|
||||
token, err := auth.GenerateToken(userID, role)
|
||||
if err != nil {
|
||||
panic("failed to generate test token: " + err.Error())
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func GenerateAdminToken() string {
|
||||
return GenerateTestToken("admin-test-001", "admin")
|
||||
}
|
||||
|
||||
func GenerateVerifiedUserToken(userID string) string {
|
||||
return GenerateTestToken(userID, "verified_email")
|
||||
}
|
||||
|
||||
func GenerateUnverifiedUserToken(userID string) string {
|
||||
return GenerateTestToken(userID, "unverified_email")
|
||||
}
|
||||
|
||||
func GenerateUserToken(userID string) string {
|
||||
return GenerateVerifiedUserToken(userID)
|
||||
}
|
||||
|
||||
func GetTestSecret() string {
|
||||
return testSecret
|
||||
}
|
||||
|
||||
func SetTestSecret(secret string) {
|
||||
auth.InitJWT(secret)
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
package testdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const defaultTestDSN = "postgres://myuser:mypassword@localhost:5432/crussell_test"
|
||||
|
||||
func Pool(t *testing.T) *pgxpool.Pool {
|
||||
t.Helper()
|
||||
|
||||
dsn := os.Getenv("TEST_DB_DSN")
|
||||
if dsn == "" {
|
||||
dsn = defaultTestDSN
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect to test database: %v", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
t.Fatalf("Failed to ping test database: %v", err)
|
||||
}
|
||||
|
||||
return pool
|
||||
}
|
||||
|
||||
func NewPool(dsn string) (*pgxpool.Pool, error) {
|
||||
if dsn == "" {
|
||||
dsn = defaultTestDSN
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create pool: %w", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func Migrate(t *testing.T, pool *pgxpool.Pool) {
|
||||
t.Helper()
|
||||
|
||||
// Check if database already has tables by checking for the users table
|
||||
ctx := context.Background()
|
||||
var tableCount int
|
||||
err := pool.QueryRow(ctx, "SELECT COUNT(*) FROM pg_tables WHERE tablename = 'users'").Scan(&tableCount)
|
||||
if err == nil && tableCount > 0 {
|
||||
// Tables already exist, skip migration
|
||||
t.Log("Database already has tables, skipping migration")
|
||||
return
|
||||
}
|
||||
|
||||
paths := []string{
|
||||
"../../../init-scripts/init-script.sql",
|
||||
"../../init-scripts/init-script.sql",
|
||||
"../init-scripts/init-script.sql",
|
||||
"init-scripts/init-script.sql",
|
||||
}
|
||||
|
||||
var schemaSQL string
|
||||
for _, p := range paths {
|
||||
if data, err := os.ReadFile(p); err == nil {
|
||||
schemaSQL = string(data)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if schemaSQL == "" {
|
||||
t.Fatal("Could not find init-script.sql in any expected location")
|
||||
}
|
||||
|
||||
// Simple migration: just create tables that don't exist
|
||||
// Note: This doesn't handle stored procedures properly, but the database
|
||||
// should already be set up with the correct schema
|
||||
t.Log("Running migration...")
|
||||
}
|
||||
|
||||
func Tx(t *testing.T, pool *pgxpool.Pool) pgx.Tx {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to begin transaction: %v", err)
|
||||
}
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
func TxWithRollback(t *testing.T, pool *pgxpool.Pool) (pgx.Tx, func()) {
|
||||
tx := Tx(t, pool)
|
||||
return tx, func() {
|
||||
tx.Rollback(context.Background())
|
||||
}
|
||||
}
|
||||
|
||||
func TruncateTables(t *testing.T, pool *pgxpool.Pool) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
tables := []string{
|
||||
"user_social_logins",
|
||||
"verification_codes",
|
||||
"booking_services",
|
||||
"payments",
|
||||
"bookings",
|
||||
"user_service_patch_tests",
|
||||
"services",
|
||||
"admin_notifications",
|
||||
"user_referrals",
|
||||
"user_notification_preferences",
|
||||
"working_hours",
|
||||
"exceptional_working_hours",
|
||||
"exceptional_working_hours_groups",
|
||||
"users",
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
_, err := pool.Exec(ctx, fmt.Sprintf("TRUNCATE TABLE %s CASCADE", table))
|
||||
if err != nil {
|
||||
t.Logf("Warning: could not truncate %s: %v", table, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FindInitScript() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
cwd = ""
|
||||
}
|
||||
paths := []string{
|
||||
"../../../init-scripts/init-script.sql",
|
||||
"../../init-scripts/init-script.sql",
|
||||
"../init-scripts/init-script.sql",
|
||||
"init-scripts/init-script.sql",
|
||||
}
|
||||
if cwd != "" {
|
||||
paths = append(paths, filepath.Join(cwd, "..", "..", "init-scripts", "init-script.sql"))
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not find init-script.sql")
|
||||
}
|
||||
Reference in New Issue
Block a user