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,468 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
package portfolio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"crussell/db"
|
||||
"crussell/mw"
|
||||
"crussell/testutils/jwt"
|
||||
"crussell/testutils/testdb"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) func() {
|
||||
t.Helper()
|
||||
|
||||
pool := testdb.Pool(t)
|
||||
testdb.Migrate(t, pool)
|
||||
|
||||
originalDB := db.DB
|
||||
db.DB = pool
|
||||
|
||||
jwt.Init()
|
||||
|
||||
return func() {
|
||||
db.DB = originalDB
|
||||
pool.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func makeRequest(handler http.HandlerFunc, method, path string, body interface{}) *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)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func makeRequestWithContext(handler http.HandlerFunc, method, path string, body interface{}, userID, role 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)
|
||||
}
|
||||
|
||||
// Add user context
|
||||
ctx := req.Context()
|
||||
ctx = context.WithValue(ctx, mw.UserIDKey, userID)
|
||||
ctx = context.WithValue(ctx, mw.UserRoleKey, role)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// List Images Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestPortfolio_ListImages(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Insert test images
|
||||
_, err := db.DB.Exec(context.Background(), `
|
||||
INSERT INTO images (url, thumbnail_url, tag_names)
|
||||
VALUES
|
||||
('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest', 'color:green']),
|
||||
('https://example.com/img2.jpg', 'https://example.com/img2_thumb.jpg', ARRAY['nature:ocean', 'color:blue'])
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create images: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(ListImages)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/images", nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var images []Image
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if len(images) != 2 {
|
||||
t.Errorf("expected 2 images, got %d", len(images))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_ListImages_WithTagFilter(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Insert test images
|
||||
_, err := db.DB.Exec(context.Background(), `
|
||||
INSERT INTO images (url, thumbnail_url, tag_names)
|
||||
VALUES
|
||||
('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest']),
|
||||
('https://example.com/img2.jpg', 'https://example.com/img2_thumb.jpg', ARRAY['nature:ocean'])
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create images: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(ListImages)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/images?tag=forest", nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var images []Image
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if len(images) != 1 {
|
||||
t.Errorf("expected 1 image, got %d", len(images))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_ListImages_Empty(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
handler := http.HandlerFunc(ListImages)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/images", nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var images []Image
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if len(images) != 0 {
|
||||
t.Errorf("expected 0 images, got %d", len(images))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// List Tags Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestPortfolio_ListTags(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Insert test tags
|
||||
_, err := db.DB.Exec(context.Background(), `
|
||||
INSERT INTO tags (name) VALUES ('nature:forest'), ('nature:ocean'), ('color:green')
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create tags: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(ListTags)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/tags", nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var tags []Tag
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &tags); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if len(tags) != 3 {
|
||||
t.Errorf("expected 3 tags, got %d", len(tags))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_ListTags_WithQuery(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Insert test tags
|
||||
_, err := db.DB.Exec(context.Background(), `
|
||||
INSERT INTO tags (name) VALUES ('nature:forest'), ('nature:ocean'), ('color:green')
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create tags: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(ListTags)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/tags?q=forest", nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var tags []Tag
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &tags); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if len(tags) != 1 {
|
||||
t.Errorf("expected 1 tag, got %d", len(tags))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_ListTags_Empty(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
handler := http.HandlerFunc(ListTags)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/tags", nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var tags []Tag
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &tags); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if len(tags) != 0 {
|
||||
t.Errorf("expected 0 tags, got %d", len(tags))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// List Filters Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestPortfolio_ListFilters(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Insert test images with tags
|
||||
_, err := db.DB.Exec(context.Background(), `
|
||||
INSERT INTO images (url, thumbnail_url, tag_names)
|
||||
VALUES
|
||||
('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest', 'color:green']),
|
||||
('https://example.com/img2.jpg', 'https://example.com/img2_thumb.jpg', ARRAY['nature:ocean', 'color:blue'])
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create images: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(ListFilters)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/filters", nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var filters []FilterCategory
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &filters); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if len(filters) == 0 {
|
||||
t.Error("expected filters, got empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_ListFilters_Empty(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
handler := http.HandlerFunc(ListFilters)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/filters", nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var filters []FilterCategory
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &filters); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if len(filters) != 0 {
|
||||
t.Errorf("expected 0 filters, got %d", len(filters))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Get Image Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestPortfolio_GetImage(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Insert test image
|
||||
var imageID string
|
||||
err := db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO images (url, thumbnail_url, tag_names)
|
||||
VALUES ('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest'])
|
||||
RETURNING id
|
||||
`).Scan(&imageID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create image: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(GetImage)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/images/"+imageID, nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var img Image
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &img); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if img.ID != imageID {
|
||||
t.Errorf("expected image ID %s, got %s", imageID, img.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_GetImage_NotFound(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
handler := http.HandlerFunc(GetImage)
|
||||
w := makeRequest(handler, "GET", "/api/portfolio/images/nonexistent-id", nil)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Upload Image Tests (Admin Only)
|
||||
// =============================================================================
|
||||
|
||||
func TestPortfolio_Upload_Admin(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a minimal S3 client mock by setting it to nil (handler will check and return error)
|
||||
// The handler requires S3 client, so we test the auth check first
|
||||
// Since S3 client setup is complex, we test that admin gets past auth check
|
||||
|
||||
handler := http.HandlerFunc(UploadImage)
|
||||
w := makeRequestWithContext(handler, "POST", "/api/portfolio/images", nil, "admin-001", "admin")
|
||||
|
||||
// Should not get 403 (forbidden), will get another error due to missing S3 or file
|
||||
// The important thing is it's not 403 for admin
|
||||
if w.Code == http.StatusForbidden {
|
||||
t.Error("admin should not get 403 - admin access should be granted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_Upload_NonAdmin(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
handler := http.HandlerFunc(UploadImage)
|
||||
w := makeRequestWithContext(handler, "POST", "/api/portfolio/images", nil, "user-001", "verified_email")
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_Upload_Unauthenticated(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
handler := http.HandlerFunc(UploadImage)
|
||||
w := makeRequest(handler, "POST", "/api/portfolio/images", nil)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Delete Image Tests (Admin Only)
|
||||
// =============================================================================
|
||||
|
||||
func TestPortfolio_Delete_Admin(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Insert test image
|
||||
var imageID string
|
||||
err := db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO images (url, thumbnail_url, tag_names)
|
||||
VALUES ('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest'])
|
||||
RETURNING id
|
||||
`).Scan(&imageID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create image: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(DeleteImage)
|
||||
w := makeRequestWithContext(handler, "DELETE", "/api/portfolio/images/"+imageID, nil, "admin-001", "admin")
|
||||
|
||||
// Should not get 403 (forbidden) - will get error due to S3 client being nil
|
||||
// but the important thing is admin auth passed
|
||||
if w.Code == http.StatusForbidden {
|
||||
t.Error("admin should not get 403 - admin access should be granted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_Delete_NonAdmin(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Insert test image
|
||||
var imageID string
|
||||
err := db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO images (url, thumbnail_url, tag_names)
|
||||
VALUES ('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest'])
|
||||
RETURNING id
|
||||
`).Scan(&imageID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create image: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(DeleteImage)
|
||||
w := makeRequestWithContext(handler, "DELETE", "/api/portfolio/images/"+imageID, nil, "user-001", "verified_email")
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected status 403, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortfolio_Delete_Unauthenticated(t *testing.T) {
|
||||
cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Insert test image
|
||||
var imageID string
|
||||
err := db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO images (url, thumbnail_url, tag_names)
|
||||
VALUES ('https://example.com/img1.jpg', 'https://example.com/img1_thumb.jpg', ARRAY['nature:forest'])
|
||||
RETURNING id
|
||||
`).Scan(&imageID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create image: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(DeleteImage)
|
||||
w := makeRequest(handler, "DELETE", "/api/portfolio/images/"+imageID, nil)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user