package db import ( "context" "fmt" "testing" "time" "golang.org/x/crypto/bcrypt" ) func newTestDB(t *testing.T) *DB { t.Helper() db, err := Open(":memory:") if err != nil { t.Fatalf("open :memory: %v", err) } t.Cleanup(func() { db.Close() }) if err := db.Migrate(context.Background()); err != nil { t.Fatalf("migrate: %v", err) } return db } func futureTime() time.Time { return time.Now().UTC().Add(24 * time.Hour) } func hashPassword(t *testing.T, pw string) string { t.Helper() h, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) if err != nil { t.Fatal(err) } return string(h) } // ─── Users ────────────────────────────────────────────────────────────── func TestCreateUser(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, err := d.CreateUser(ctx, "alice", hashPassword(t, "password1"), false) if err != nil { t.Fatalf("CreateUser: %v", err) } if u.ID == 0 { t.Fatal("expected non-zero ID") } if u.Username != "alice" { t.Fatalf("username = %q, want %q", u.Username, "alice") } if u.IsAdmin { t.Fatal("expected non-admin") } if u.CreatedAt.IsZero() { t.Fatal("expected created_at") } } func TestCreateUser_DuplicateUsername(t *testing.T) { d := newTestDB(t) ctx := context.Background() d.CreateUser(ctx, "bob", hashPassword(t, "pw1"), false) _, err := d.CreateUser(ctx, "bob", hashPassword(t, "pw2"), false) if err == nil { t.Fatal("expected error on duplicate username") } } func TestGetUserByID(t *testing.T) { d := newTestDB(t) ctx := context.Background() created, _ := d.CreateUser(ctx, "charlie", hashPassword(t, "pw"), true) got, err := d.GetUserByID(ctx, created.ID) if err != nil { t.Fatalf("GetUserByID: %v", err) } if got.Username != "charlie" { t.Fatalf("username = %q", "charlie") } if !got.IsAdmin { t.Fatal("expected admin") } } func TestGetUserByID_NotFound(t *testing.T) { d := newTestDB(t) _, err := d.GetUserByID(context.Background(), 999) if err == nil { t.Fatal("expected error for non-existent user") } } func TestGetUserByUsername(t *testing.T) { d := newTestDB(t) ctx := context.Background() d.CreateUser(ctx, "dave", hashPassword(t, "pw"), false) got, err := d.GetUserByUsername(ctx, "dave") if err != nil { t.Fatalf("GetUserByUsername: %v", err) } if got.Username != "dave" { t.Fatalf("username = %q", "dave") } } func TestListUsers(t *testing.T) { d := newTestDB(t) ctx := context.Background() users, err := d.ListUsers(ctx) if err != nil { t.Fatalf("ListUsers (empty): %v", err) } if len(users) != 0 { t.Fatalf("expected 0 users, got %d", len(users)) } d.CreateUser(ctx, "zara", hashPassword(t, "pw"), false) d.CreateUser(ctx, "adam", hashPassword(t, "pw"), false) users, _ = d.ListUsers(ctx) if len(users) != 2 { t.Fatalf("expected 2 users, got %d", len(users)) } if users[0].Username != "adam" { t.Fatalf("expected first user adam, got %s", users[0].Username) } if users[1].Username != "zara" { t.Fatalf("expected second user zara, got %s", users[1].Username) } } func TestDeleteUser(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "eve", hashPassword(t, "pw"), false) if err := d.DeleteUser(ctx, u.ID); err != nil { t.Fatalf("DeleteUser: %v", err) } // Verify deleted. users, _ := d.ListUsers(ctx) if len(users) != 0 { t.Fatalf("expected 0 users after delete") } } func TestDeleteUser_NotFound(t *testing.T) { d := newTestDB(t) err := d.DeleteUser(context.Background(), 999) if err == nil { t.Fatal("expected error for non-existent user") } } func TestUpdateUserPassword(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "frank", hashPassword(t, "oldpw"), false) newHash := hashPassword(t, "newpw") if err := d.UpdateUserPassword(ctx, u.ID, newHash); err != nil { t.Fatalf("UpdateUserPassword: %v", err) } updated, _ := d.GetUserByID(ctx, u.ID) if updated.PasswordHash == u.PasswordHash { t.Fatal("password hash should have changed") } } // ─── Sessions ─────────────────────────────────────────────────────────── func TestCreateSession(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "grace", hashPassword(t, "pw"), false) sess, err := d.CreateSession(ctx, u.ID, "tok123", futureTime()) if err != nil { t.Fatalf("CreateSession: %v", err) } if sess.ID == 0 { t.Fatal("expected non-zero session ID") } } func TestGetSessionByToken(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "heidi", hashPassword(t, "pw"), false) d.CreateSession(ctx, u.ID, "abc", futureTime()) sess, err := d.GetSessionByToken(ctx, "abc") if err != nil { t.Fatalf("GetSessionByToken: %v", err) } if sess.Token != "abc" { t.Fatalf("token = %q", "abc") } if sess.UserID != u.ID { t.Fatalf("user_id = %d, want %d", sess.UserID, u.ID) } } func TestDeleteSession(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "ivan", hashPassword(t, "pw"), false) d.CreateSession(ctx, u.ID, "tok1", futureTime()) if err := d.DeleteSession(ctx, "tok1"); err != nil { t.Fatalf("DeleteSession: %v", err) } _, err := d.GetSessionByToken(ctx, "tok1") if err == nil { t.Fatal("expected error after delete") } } func TestDeleteUserSessions(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "judy", hashPassword(t, "pw"), false) d.CreateSession(ctx, u.ID, "s1", futureTime()) d.CreateSession(ctx, u.ID, "s2", futureTime()) if err := d.DeleteUserSessions(ctx, u.ID); err != nil { t.Fatalf("DeleteUserSessions: %v", err) } _, err := d.GetSessionByToken(ctx, "s1") if err == nil { t.Fatal("expected session s1 to be deleted") } _, err = d.GetSessionByToken(ctx, "s2") if err == nil { t.Fatal("expected session s2 to be deleted") } } func TestExtendSession(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "karl", hashPassword(t, "pw"), false) d.CreateSession(ctx, u.ID, "ext", futureTime()) newExpiry := futureTime() if err := d.ExtendSession(ctx, "ext", newExpiry); err != nil { t.Fatalf("ExtendSession: %v", err) } sess, _ := d.GetSessionByToken(ctx, "ext") if sess.ExpiresAt.Unix() != newExpiry.Unix() { t.Fatalf("expiry = %v, want %v", sess.ExpiresAt, newExpiry) } } // ─── Images & Tags ────────────────────────────────────────────────────── func TestCreateAndGetImage(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "laurie", hashPassword(t, "pw"), false) img, err := d.CreateImage(ctx, &Image{ Filename: "photo.jpg", StoragePath: "ab/cd/ef/abc123.jpg", UploadedBy: u.ID, FileSize: 1024, Width: 800, Height: 600, MimeType: "image/jpeg", }) if err != nil { t.Fatalf("CreateImage: %v", err) } if img.ID == 0 { t.Fatal("expected non-zero ID") } got, err := d.GetImageByID(ctx, img.ID) if err != nil { t.Fatalf("GetImageByID: %v", err) } if got.Filename != "photo.jpg" { t.Fatalf("filename = %q", "photo.jpg") } if got.Width != 800 { t.Fatalf("width = %d", 800) } } func TestListImages_Pagination(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "mia", hashPassword(t, "pw"), false) for i := range 5 { d.CreateImage(ctx, &Image{ Filename: fmt.Sprintf("%d.jpg", i), StoragePath: fmt.Sprintf("a/b/c/%d.jpg", i), UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) } // Test full list. list, err := d.ListImages(ctx, nil, "", 10, 0, false, false) if err != nil { t.Fatalf("ListImages: %v", err) } if len(list.Images) != 5 { t.Fatalf("expected 5 images, got %d", len(list.Images)) } if list.Total != 5 { t.Fatalf("total = %d, want 5", list.Total) } // Test pagination (newest-first). list, _ = d.ListImages(ctx, nil, "", 2, 0, false, false) if len(list.Images) != 2 { t.Fatalf("expected 2 images, got %d", len(list.Images)) } if list.Total != 5 { t.Fatalf("total = %d, want 5", list.Total) } } func TestSetImageTags(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "nick", hashPassword(t, "pw"), false) img, _ := d.CreateImage(ctx, &Image{ Filename: "tagged.jpg", StoragePath: "a/b/c/t.jpg", UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) if err := d.SetImageTags(ctx, img.ID, []string{"funny", "meme"}); err != nil { t.Fatalf("SetImageTags: %v", err) } tags, err := d.GetImageTags(ctx, img.ID) if err != nil { t.Fatalf("GetImageTags: %v", err) } if len(tags) != 2 || tags[0] != "funny" || tags[1] != "meme" { t.Fatalf("tags = %v, want [funny meme]", tags) } } func TestSetImageTags_Replace(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "oscar", hashPassword(t, "pw"), false) img, _ := d.CreateImage(ctx, &Image{ Filename: "replace.jpg", StoragePath: "a/b/c/r.jpg", UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) d.SetImageTags(ctx, img.ID, []string{"old1", "old2"}) d.SetImageTags(ctx, img.ID, []string{"new1"}) tags, _ := d.GetImageTags(ctx, img.ID) if len(tags) != 1 || tags[0] != "new1" { t.Fatalf("tags = %v, want [new1]", tags) } } func TestTagUsageCounts(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "pat", hashPassword(t, "pw"), false) img1, _ := d.CreateImage(ctx, &Image{Filename: "1.jpg", StoragePath: "a/b/c/1.jpg", UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg"}) img2, _ := d.CreateImage(ctx, &Image{Filename: "2.jpg", StoragePath: "a/b/c/2.jpg", UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg"}) d.SetImageTags(ctx, img1.ID, []string{"funny"}) d.SetImageTags(ctx, img2.ID, []string{"funny", "meme"}) suggestions, err := d.SuggestTags(ctx, "", 10) if err != nil { t.Fatalf("SuggestTags: %v", err) } // funny should have count 2, meme count 1. if len(suggestions) != 2 { t.Fatalf("expected 2 tag suggestions, got %d", len(suggestions)) } if suggestions[0].Tag != "funny" || suggestions[0].UsageCount != 2 { t.Fatalf("expected funny (2), got %s (%d)", suggestions[0].Tag, suggestions[0].UsageCount) } if suggestions[1].Tag != "meme" || suggestions[1].UsageCount != 1 { t.Fatalf("expected meme (1), got %s (%d)", suggestions[1].Tag, suggestions[1].UsageCount) } } func TestDeleteImage(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "quinn", hashPassword(t, "pw"), false) img, _ := d.CreateImage(ctx, &Image{Filename: "del.jpg", StoragePath: "a/b/c/d.jpg", UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg"}) d.SetImageTags(ctx, img.ID, []string{"delete_me"}) if err := d.DeleteImage(ctx, img.ID); err != nil { t.Fatalf("DeleteImage: %v", err) } // Verify image gone. _, err := d.GetImageByID(ctx, img.ID) if err == nil { t.Fatal("expected error after image deletion") } // Verify tag usage count decremented. suggestions, _ := d.SuggestTags(ctx, "", 10) if len(suggestions) != 0 { t.Fatalf("expected 0 tag after delete, got %d", len(suggestions)) } } func TestSuggestTags(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "ray", hashPassword(t, "pw"), false) img, _ := d.CreateImage(ctx, &Image{Filename: "s.jpg", StoragePath: "a/b/c/s.jpg", UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg"}) d.SetImageTags(ctx, img.ID, []string{"landscape", "landscape_sunset", "portrait"}) tests := []struct { name string prefix string want int }{ {"match landscape prefix", "land", 2}, {"match port prefix", "port", 1}, {"no match", "xyz", 0}, {"empty prefix returns all", "", 3}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { results, err := d.SuggestTags(ctx, tt.prefix, 10) if err != nil { t.Fatalf("SuggestTags(%q): %v", tt.prefix, err) } if len(results) != tt.want { t.Fatalf("SuggestTags(%q) = %d results, want %d", tt.prefix, len(results), tt.want) } }) } } func TestNormalizeTag(t *testing.T) { tests := []struct { input string want string }{ {" Funny ", "funny"}, {"MEME", "meme"}, {" hello world ", "hello world"}, {"", ""}, {" ", ""}, } for _, tt := range tests { got := normalizeTag(tt.input) if got != tt.want { t.Errorf("normalizeTag(%q) = %q, want %q", tt.input, got, tt.want) } } } func TestListImages_UploaderFilter(t *testing.T) { d := newTestDB(t) ctx := context.Background() u1, _ := d.CreateUser(ctx, "alice", hashPassword(t, "pw"), false) u2, _ := d.CreateUser(ctx, "bob", hashPassword(t, "pw"), false) for i := range 3 { uid := u1.ID if i == 2 { uid = u2.ID } d.CreateImage(ctx, &Image{ Filename: fmt.Sprintf("%d.jpg", i), StoragePath: fmt.Sprintf("a/b/c/%d.jpg", i), UploadedBy: uid, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) } tests := []struct { name string uploader string want int }{ {name: "filter alice (2 images)", uploader: "alice", want: 2}, {name: "filter bob (1 image)", uploader: "bob", want: 1}, {name: "no match", uploader: "nobody", want: 0}, {name: "empty filter returns all", uploader: "", want: 3}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := d.ListImages(ctx, nil, tt.uploader, 10, 0, false, false) if err != nil { t.Fatalf("ListImages: %v", err) } if len(result.Images) != tt.want { t.Errorf("got %d images, want %d", len(result.Images), tt.want) } if result.Total != tt.want { t.Errorf("total = %d, want %d", result.Total, tt.want) } }) } } func TestListImages_TagAndUploaderFilter(t *testing.T) { d := newTestDB(t) ctx := context.Background() u1, _ := d.CreateUser(ctx, "alice", hashPassword(t, "pw"), false) u2, _ := d.CreateUser(ctx, "bob", hashPassword(t, "pw"), false) img1, _ := d.CreateImage(ctx, &Image{ Filename: "a.jpg", StoragePath: "a/b/c/a.jpg", UploadedBy: u1.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) img2, _ := d.CreateImage(ctx, &Image{ Filename: "b.jpg", StoragePath: "a/b/c/b.jpg", UploadedBy: u1.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) img3, _ := d.CreateImage(ctx, &Image{ Filename: "c.jpg", StoragePath: "a/b/c/c.jpg", UploadedBy: u2.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) d.SetImageTags(ctx, img1.ID, []string{"landscape"}) d.SetImageTags(ctx, img2.ID, []string{"landscape"}) d.SetImageTags(ctx, img3.ID, []string{"landscape"}) tests := []struct { name string tags []string uploader string want int }{ {name: "tag + uploader alice", tags: []string{"landscape"}, uploader: "alice", want: 2}, {name: "tag + uploader bob", tags: []string{"landscape"}, uploader: "bob", want: 1}, {name: "tag only", tags: []string{"landscape"}, uploader: "", want: 3}, {name: "uploader only", tags: nil, uploader: "alice", want: 2}, {name: "no match tag", tags: []string{"sunset"}, uploader: "alice", want: 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := d.ListImages(ctx, tt.tags, tt.uploader, 10, 0, false, false) if err != nil { t.Fatalf("ListImages: %v", err) } if len(result.Images) != tt.want { t.Errorf("got %d images, want %d", len(result.Images), tt.want) } if result.Total != tt.want { t.Errorf("total = %d, want %d", result.Total, tt.want) } }) } } func TestSuggestUploaders(t *testing.T) { d := newTestDB(t) ctx := context.Background() u1, _ := d.CreateUser(ctx, "alice", hashPassword(t, "pw"), false) d.CreateUser(ctx, "bob", hashPassword(t, "pw"), false) // alice uploads 2 images, bob uploads 0. for i := range 2 { d.CreateImage(ctx, &Image{ Filename: fmt.Sprintf("%d.jpg", i), StoragePath: fmt.Sprintf("a/b/c/%d.jpg", i), UploadedBy: u1.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) } tests := []struct { name string prefix string want int first string count int }{ {name: "match ali prefix", prefix: "ali", want: 1, first: "alice", count: 2}, {name: "match bo prefix", prefix: "bo", want: 1, first: "bob", count: 0}, {name: "no match", prefix: "xyz", want: 0}, {name: "all with empty prefix", prefix: "", want: 2, first: "alice", count: 2}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { results, err := d.SuggestUploaders(ctx, tt.prefix, 10) if err != nil { t.Fatalf("SuggestUploaders: %v", err) } if len(results) != tt.want { t.Fatalf("got %d results, want %d", len(results), tt.want) } if tt.want > 0 { if results[0].Tag != tt.first { t.Errorf("first result = %q, want %q", results[0].Tag, tt.first) } if results[0].UsageCount != tt.count { t.Errorf("first result count = %d, want %d", results[0].UsageCount, tt.count) } } }) } } // ─── Innate Tags ──────────────────────────────────────────────────────── func TestCreateImageWithInnateTags(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "innate_user", hashPassword(t, "pw"), false) img, err := d.CreateImage(ctx, &Image{ Filename: "innate.jpg", StoragePath: "a/b/c/innate.jpg", UploadedBy: u.ID, FileSize: 1024, Width: 1920, Height: 1080, MimeType: "image/jpeg", InnateTags: []string{"image", "highres"}, }) if err != nil { t.Fatalf("CreateImage: %v", err) } if len(img.InnateTags) != 2 { t.Fatalf("expected 2 innate tags, got %d: %v", len(img.InnateTags), img.InnateTags) } if img.InnateTags[0] != "image" || img.InnateTags[1] != "highres" { t.Fatalf("innate tags = %v, want [image highres]", img.InnateTags) } } func TestCreateImageWithEmptyInnateTags(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "no_innate", hashPassword(t, "pw"), false) img, err := d.CreateImage(ctx, &Image{ Filename: "plain.jpg", StoragePath: "a/b/c/plain.jpg", UploadedBy: u.ID, FileSize: 1024, Width: 800, Height: 600, MimeType: "image/jpeg", }) if err != nil { t.Fatalf("CreateImage: %v", err) } if len(img.InnateTags) != 0 { t.Fatalf("expected 0 innate tags, got %d: %v", len(img.InnateTags), img.InnateTags) } } func TestListImages_TagFilterIncludesInnateTags(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "tagger", hashPassword(t, "pw"), false) // Image with regular tags only. img1, _ := d.CreateImage(ctx, &Image{ Filename: "1.jpg", StoragePath: "a/b/c/1.jpg", UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) d.SetImageTags(ctx, img1.ID, []string{"landscape"}) // Image with innate tags and no user tags. d.CreateImage(ctx, &Image{ Filename: "2.jpg", StoragePath: "a/b/c/2.jpg", UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", InnateTags: []string{"lowres"}, }) // Image with both user tags and innate tags. img3, _ := d.CreateImage(ctx, &Image{ Filename: "3.jpg", StoragePath: "a/b/c/3.jpg", UploadedBy: u.ID, FileSize: 1024, Width: 2560, Height: 1440, MimeType: "image/jpeg", InnateTags: []string{"highres"}, }) d.SetImageTags(ctx, img3.ID, []string{"landscape"}) tests := []struct { name string tags []string want int }{ {name: "filter by regular tag", tags: []string{"landscape"}, want: 2}, {name: "filter by innate tag", tags: []string{"lowres"}, want: 1}, {name: "filter by innate tag highres", tags: []string{"highres"}, want: 1}, {name: "combined regular+innate tag", tags: []string{"landscape", "highres"}, want: 1}, {name: "no match", tags: []string{"nonexistent"}, want: 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := d.ListImages(ctx, tt.tags, "", 10, 0, false, false) if err != nil { t.Fatalf("ListImages: %v", err) } if len(result.Images) != tt.want { t.Errorf("got %d images, want %d; tags=%v", len(result.Images), tt.want, tt.tags) } }) } } func TestListImages_TagFilterInnateAndUploader(t *testing.T) { d := newTestDB(t) ctx := context.Background() u1, _ := d.CreateUser(ctx, "alice", hashPassword(t, "pw"), false) u2, _ := d.CreateUser(ctx, "bob", hashPassword(t, "pw"), false) img1, _ := d.CreateImage(ctx, &Image{ Filename: "a.jpg", StoragePath: "a/b/c/a.jpg", UploadedBy: u1.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", InnateTags: []string{"lowres"}, }) img2, _ := d.CreateImage(ctx, &Image{ Filename: "b.jpg", StoragePath: "a/b/c/b.jpg", UploadedBy: u1.ID, FileSize: 1000, Width: 2560, Height: 1440, MimeType: "image/jpeg", InnateTags: []string{"highres"}, }) img3, _ := d.CreateImage(ctx, &Image{ Filename: "c.jpg", StoragePath: "a/b/c/c.jpg", UploadedBy: u2.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", InnateTags: []string{"lowres"}, }) d.SetImageTags(ctx, img1.ID, []string{"landscape"}) d.SetImageTags(ctx, img2.ID, []string{"landscape"}) d.SetImageTags(ctx, img3.ID, []string{"landscape"}) tests := []struct { name string tags []string uploader string want int }{ {name: "lowres+alice", tags: []string{"lowres"}, uploader: "alice", want: 1}, {name: "highres+alice", tags: []string{"highres"}, uploader: "alice", want: 1}, {name: "lowres+bob", tags: []string{"lowres"}, uploader: "bob", want: 1}, {name: "landscape+highres+alice", tags: []string{"landscape", "highres"}, uploader: "alice", want: 1}, {name: "no match", tags: []string{"highres"}, uploader: "bob", want: 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := d.ListImages(ctx, tt.tags, tt.uploader, 10, 0, false, false) if err != nil { t.Fatalf("ListImages: %v", err) } if len(result.Images) != tt.want { t.Errorf("got %d images, want %d; tags=%v uploader=%s", len(result.Images), tt.want, tt.tags, tt.uploader) } }) } } func TestListAllImages(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "user", hashPassword(t, "pw"), false) // Create two images. for i := range 2 { _, err := d.CreateImage(ctx, &Image{ Filename: fmt.Sprintf("%d.jpg", i), StoragePath: fmt.Sprintf("a/b/c/%d.jpg", i), UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) if err != nil { t.Fatalf("CreateImage: %v", err) } } images, err := d.ListAllImages(ctx) if err != nil { t.Fatalf("ListAllImages: %v", err) } if len(images) != 2 { t.Fatalf("got %d images, want 2", len(images)) } } func TestUpdateImageInnateTags(t *testing.T) { d := newTestDB(t) ctx := context.Background() u, _ := d.CreateUser(ctx, "user", hashPassword(t, "pw"), false) img, _ := d.CreateImage(ctx, &Image{ Filename: "test.jpg", StoragePath: "a/b/c/test.jpg", UploadedBy: u.ID, FileSize: 100, Width: 100, Height: 100, MimeType: "image/jpeg", }) if err := d.UpdateImageInnateTags(ctx, img.ID, []string{"image", "lowres"}); err != nil { t.Fatalf("UpdateImageInnateTags: %v", err) } got, _ := d.GetImageByID(ctx, img.ID) if len(got.InnateTags) != 2 { t.Fatalf("expected 2 innate tags, got %d: %v", len(got.InnateTags), got.InnateTags) } if got.InnateTags[0] != "image" || got.InnateTags[1] != "lowres" { t.Fatalf("innate tags = %v, want [image lowres]", got.InnateTags) } }