test: add coverage tests across backend + fix mock for PENDING checkout support
CI / Nginx config check (push) Successful in 13s
CI / Env docs check (push) Successful in 15s
CI / Docker compose check (push) Successful in 15s
CI / Frontend major deps (push) Failing after 24s
CI / Frontend deps check (push) Successful in 30s
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 1m3s
CI / Knip (push) Successful in 45s
CI / Go vet (prod) (push) Failing after 1m42s
CI / Frontend a11y check (push) Successful in 2m34s
CI / Go vet (dev) (push) Successful in 2m29s
CI / Staticcheck (prod) (push) Failing after 2m38s
CI / go mod tidy (push) Successful in 1m3s
CI / Staticcheck (dev) (push) Successful in 2m55s
CI / Frontend QC (audit) (push) Successful in 51s
CI / golangci-lint (push) Successful in 3m22s
CI / Go vulnerabilities (push) Successful in 1m26s
CI / Frontend QC (typecheck) (push) Successful in 2m18s
CI / Security scan (prod) (push) Successful in 4m18s
CI / Security scan (dev) (push) Successful in 4m40s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
CI / Frontend QC (lint) (push) Successful in 2m18s
CI / Svelte strict check (push) Successful in 43s

New test files cover previously untested paths across DAV, validators,
S3, Square, mw, bookings, user, and payments packages.

Includes mock fix: HoldCheckouts flag on MockClient allows tests to
pause auto-complete goroutine for testing PENDING checkout states.

Coverage: 50.4% → 65.0% (+14.6pp)
This commit is contained in:
2026-07-10 18:13:44 +01:00
parent c0442d4ebd
commit 3029fd5179
57 changed files with 12604 additions and 94 deletions
+331
View File
@@ -1404,3 +1404,334 @@ func TestPortfolio_ListFilters_WithMultiFormatImages(t *testing.T) {
t.Error("expected filters, got empty")
}
}
func avifBytes() []byte {
data := make([]byte, 12)
copy(data[4:8], "ftyp")
copy(data[8:12], "avif")
return data
}
func webpBytes() []byte {
data := make([]byte, 12)
copy(data[0:4], "RIFF")
copy(data[8:12], "WEBP")
return data
}
func jxlBytes() []byte {
data := make([]byte, 12)
copy(data[4:8], "ftyp")
copy(data[8:12], "jxl ")
return data
}
// =============================================================================
// Upload Image With Mock S3 — Full Flow
// =============================================================================
// TestPortfolio_Upload_Success verifies a successful image upload with mock S3,
// testing the full flow: multipart form parsing, S3 upload, DB insert, and response.
func TestPortfolio_Upload_Success(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
jpeg := jpegBytes(t)
avif := avifBytes()
webp := webpBytes()
jxl := jxlBytes()
var b bytes.Buffer
w := multipart.NewWriter(&b)
// Required full format fields
fw, _ := w.CreateFormFile("file_full_avif", "test.avif")
fw.Write(avif)
fw, _ = w.CreateFormFile("file_full_webp", "test.webp")
fw.Write(webp)
fw, _ = w.CreateFormFile("file_full_jpg", "test.jpg")
fw.Write(jpeg)
// Optional full format field
fw, _ = w.CreateFormFile("file_full_jxl", "test.jxl")
fw.Write(jxl)
// Required thumb format fields
fw, _ = w.CreateFormFile("file_thumb_avif", "thumb.avif")
fw.Write(avif)
fw, _ = w.CreateFormFile("file_thumb_webp", "thumb.webp")
fw.Write(webp)
fw, _ = w.CreateFormFile("file_thumb_jpg", "thumb.jpg")
fw.Write(jpeg)
// Text fields
w.WriteField("category", "manicure")
w.WriteField("tags", "color:red,style:art")
w.Close()
req := httptest.NewRequest("POST", "/api/portfolio/images", &b)
req.Header.Set("Content-Type", w.FormDataContentType())
chiCtx := context.WithValue(ctx, mw.UserIDKey, "admin-001")
chiCtx = context.WithValue(chiCtx, mw.UserRoleKey, "admin")
req = req.WithContext(chiCtx)
rec := httptest.NewRecorder()
UploadImage(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", rec.Code, rec.Body.String())
}
var img Image
if err := json.Unmarshal(rec.Body.Bytes(), &img); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
if img.ID == "" {
t.Error("expected image ID to be non-empty")
}
if img.URL == "" {
t.Error("expected image URL to be non-empty")
}
if img.ThumbnailURL == "" {
t.Error("expected thumbnail URL to be non-empty")
}
if len(img.TagNames) != 2 {
t.Errorf("expected 2 tags, got %d: %v", len(img.TagNames), img.TagNames)
}
// Verify full format URLs
if img.Full.Avif == "" {
t.Error("expected full.avif URL")
}
if img.Full.Webp == "" {
t.Error("expected full.webp URL")
}
if img.Full.Jpg == "" {
t.Error("expected full.jpg URL")
}
if img.Full.Jxl == "" {
t.Error("expected full.jxl URL")
}
// Verify thumb format URLs
if img.Thumb.Avif == "" {
t.Error("expected thumb.avif URL")
}
if img.Thumb.Webp == "" {
t.Error("expected thumb.webp URL")
}
if img.Thumb.Jpg == "" {
t.Error("expected thumb.jpg URL")
}
}
// TestPortfolio_Upload_NoAuth verifies that unauthenticated requests receive
// 401 Unauthorized (auth check occurs before any body processing).
func TestPortfolio_Upload_NoAuth(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UploadImage)
w := makeRequest(handler, "POST", "/api/portfolio/images", nil, ctx)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d. body: %s", w.Code, w.Body.String())
}
}
// =============================================================================
// List Filters — Category & Tag Filter Tests
// =============================================================================
// TestPortfolio_ListFilters_WithCategoryFilter verifies that ListFilters
// correctly returns both selected and unselected categories when a category
// filter (e.g. ?filter[color]=green) is applied.
func TestPortfolio_ListFilters_WithCategoryFilter(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `
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']),
('https://example.com/img3.jpg', 'https://example.com/img3_thumb.jpg', ARRAY['nature:ocean', 'color:green'])
`)
if err != nil {
t.Fatalf("failed to create images: %v", err)
}
handler := http.HandlerFunc(ListFilters)
w := makeRequest(handler, "GET", "/api/portfolio/filters?filter[color]=green", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected 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.Fatal("expected at least one filter category")
}
// Build lookup maps for easier assertion
catMap := make(map[string]map[string]int)
for _, fc := range filters {
valMap := make(map[string]int)
for _, fv := range fc.Values {
valMap[fv.Value] = fv.Count
}
catMap[fc.Category] = valMap
}
// The 'color' category (selected) should show all values without filter applied
colorVals, ok := catMap["color"]
if !ok {
t.Fatal("expected 'color' category in filters")
}
if colorVals["green"] != 2 {
t.Errorf("expected color:green count 2, got %d", colorVals["green"])
}
if colorVals["blue"] != 1 {
t.Errorf("expected color:blue count 1, got %d", colorVals["blue"])
}
// The 'nature' category (unselected) should be filtered by color:green
natureVals, ok := catMap["nature"]
if !ok {
t.Fatal("expected 'nature' category in filters")
}
if natureVals["forest"] != 1 {
t.Errorf("expected nature:forest count 1, got %d", natureVals["forest"])
}
if natureVals["ocean"] != 1 {
t.Errorf("expected nature:ocean count 1, got %d", natureVals["ocean"])
}
}
// TestPortfolio_ListFilters_WithTagFilter verifies that ListFilters correctly
// scopes results when a tag filter (?tag=...) is applied.
func TestPortfolio_ListFilters_WithTagFilter(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `
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']),
('https://example.com/img3.jpg', 'https://example.com/img3_thumb.jpg', ARRAY['style:classic'])
`)
if err != nil {
t.Fatalf("failed to create images: %v", err)
}
handler := http.HandlerFunc(ListFilters)
w := makeRequest(handler, "GET", "/api/portfolio/filters?tag=nature", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected 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.Fatal("expected at least one filter category")
}
catMap := make(map[string]map[string]int)
for _, fc := range filters {
valMap := make(map[string]int)
for _, fv := range fc.Values {
valMap[fv.Value] = fv.Count
}
catMap[fc.Category] = valMap
}
// Should have 'color' and 'nature' categories from images matching 'nature' tag
if _, ok := catMap["color"]; !ok {
t.Error("expected 'color' category in results")
}
if _, ok := catMap["nature"]; !ok {
t.Error("expected 'nature' category in results")
}
// Should NOT have 'style' category (image 3 doesn't match 'nature' tag)
if _, ok := catMap["style"]; ok {
t.Error("did not expect 'style' category — image with style:classic does not match tag 'nature'")
}
}
// TestPortfolio_ListFilters_WithCombinedFilter verifies that ListFilters
// correctly scopes results when both a category filter and a tag filter
// are applied together.
func TestPortfolio_ListFilters_WithCombinedFilter(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `
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']),
('https://example.com/img3.jpg', 'https://example.com/img3_thumb.jpg', ARRAY['nature:forest', 'color:blue'])
`)
if err != nil {
t.Fatalf("failed to create images: %v", err)
}
handler := http.HandlerFunc(ListFilters)
w := makeRequest(handler, "GET", "/api/portfolio/filters?filter[color]=green&tag=nature", nil, ctx)
if w.Code != http.StatusOK {
t.Fatalf("expected 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.Fatal("expected at least one filter category")
}
catMap := make(map[string]map[string]int)
for _, fc := range filters {
valMap := make(map[string]int)
for _, fv := range fc.Values {
valMap[fv.Value] = fv.Count
}
catMap[fc.Category] = valMap
}
// Tag filter 'nature' matches all 3 images.
// Category filter 'color=green' narrows to images with color:green.
// The 'color' category (selected) shows all color values from tag-filtered images (no color filter applied).
colorVals, ok := catMap["color"]
if !ok {
t.Fatal("expected 'color' category in filters")
}
if colorVals["green"] != 1 {
t.Errorf("expected color:green count 1, got %d", colorVals["green"])
}
if colorVals["blue"] != 2 {
t.Errorf("expected color:blue count 2, got %d", colorVals["blue"])
}
// The 'nature' category (unselected) should be filtered by color:green.
natureVals, ok := catMap["nature"]
if !ok {
t.Fatal("expected 'nature' category in filters")
}
if natureVals["forest"] != 1 {
t.Errorf("expected nature:forest count 1, got %d", natureVals["forest"])
}
// nature:ocean should not appear (img2 has color:blue, not color:green)
if _, exists := natureVals["ocean"]; exists {
t.Error("did not expect nature:ocean — image with nature:ocean has color:blue, not color:green")
}
}