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
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:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user