A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
1package db
2
3import (
4 "database/sql"
5 "testing"
6)
7
8func TestAnnotations_Placeholder(t *testing.T) {
9 // Placeholder test for annotations package
10 // GetRepositoryAnnotations returns map[string]string
11 annotations := make(map[string]string)
12 annotations["test"] = "value"
13
14 if annotations["test"] != "value" {
15 t.Error("Expected annotation value to be stored")
16 }
17}
18
19// Integration tests
20
21func setupAnnotationsTestDB(t *testing.T) *sql.DB {
22 t.Helper()
23 // Use file::memory: with cache=shared to ensure all connections share the same in-memory DB
24 db, err := InitDB("file::memory:?cache=shared", true)
25 if err != nil {
26 t.Fatalf("Failed to initialize test database: %v", err)
27 }
28 // Limit to single connection to avoid race conditions in tests
29 db.SetMaxOpenConns(1)
30 t.Cleanup(func() { db.Close() })
31 return db
32}
33
34func createAnnotationTestUser(t *testing.T, db *sql.DB, did, handle string) {
35 t.Helper()
36 _, err := db.Exec(`
37 INSERT OR IGNORE INTO users (did, handle, pds_endpoint, last_seen)
38 VALUES (?, ?, ?, datetime('now'))
39 `, did, handle, "https://pds.example.com")
40 if err != nil {
41 t.Fatalf("Failed to create test user: %v", err)
42 }
43}
44
45// TestGetRepositoryAnnotations_Empty tests retrieving from empty repository
46func TestGetRepositoryAnnotations_Empty(t *testing.T) {
47 db := setupAnnotationsTestDB(t)
48
49 annotations, err := GetRepositoryAnnotations(db, "did:plc:alice123", "myapp")
50 if err != nil {
51 t.Fatalf("GetRepositoryAnnotations() error = %v", err)
52 }
53
54 if len(annotations) != 0 {
55 t.Errorf("Expected empty annotations, got %d entries", len(annotations))
56 }
57}
58
59// TestGetRepositoryAnnotations_WithData tests retrieving existing annotations
60func TestGetRepositoryAnnotations_WithData(t *testing.T) {
61 db := setupAnnotationsTestDB(t)
62 createAnnotationTestUser(t, db, "did:plc:alice123", "alice.bsky.social")
63
64 // Insert test annotations
65 testAnnotations := map[string]string{
66 "org.opencontainers.image.title": "My App",
67 "org.opencontainers.image.description": "A test application",
68 "org.opencontainers.image.version": "1.0.0",
69 }
70
71 err := UpsertRepositoryAnnotations(db, "did:plc:alice123", "myapp", testAnnotations)
72 if err != nil {
73 t.Fatalf("UpsertRepositoryAnnotations() error = %v", err)
74 }
75
76 // Retrieve annotations
77 annotations, err := GetRepositoryAnnotations(db, "did:plc:alice123", "myapp")
78 if err != nil {
79 t.Fatalf("GetRepositoryAnnotations() error = %v", err)
80 }
81
82 if len(annotations) != len(testAnnotations) {
83 t.Errorf("Expected %d annotations, got %d", len(testAnnotations), len(annotations))
84 }
85
86 for key, expectedValue := range testAnnotations {
87 if actualValue, ok := annotations[key]; !ok {
88 t.Errorf("Missing annotation key: %s", key)
89 } else if actualValue != expectedValue {
90 t.Errorf("Annotation[%s] = %v, want %v", key, actualValue, expectedValue)
91 }
92 }
93}
94
95// TestUpsertRepositoryAnnotations_Insert tests inserting new annotations
96func TestUpsertRepositoryAnnotations_Insert(t *testing.T) {
97 db := setupAnnotationsTestDB(t)
98 createAnnotationTestUser(t, db, "did:plc:bob456", "bob.bsky.social")
99
100 annotations := map[string]string{
101 "key1": "value1",
102 "key2": "value2",
103 }
104
105 err := UpsertRepositoryAnnotations(db, "did:plc:bob456", "testapp", annotations)
106 if err != nil {
107 t.Fatalf("UpsertRepositoryAnnotations() error = %v", err)
108 }
109
110 // Verify annotations were inserted
111 retrieved, err := GetRepositoryAnnotations(db, "did:plc:bob456", "testapp")
112 if err != nil {
113 t.Fatalf("GetRepositoryAnnotations() error = %v", err)
114 }
115
116 if len(retrieved) != len(annotations) {
117 t.Errorf("Expected %d annotations, got %d", len(annotations), len(retrieved))
118 }
119
120 for key, expectedValue := range annotations {
121 if actualValue := retrieved[key]; actualValue != expectedValue {
122 t.Errorf("Annotation[%s] = %v, want %v", key, actualValue, expectedValue)
123 }
124 }
125}
126
127// TestUpsertRepositoryAnnotations_Update tests updating existing annotations
128func TestUpsertRepositoryAnnotations_Update(t *testing.T) {
129 db := setupAnnotationsTestDB(t)
130 createAnnotationTestUser(t, db, "did:plc:charlie789", "charlie.bsky.social")
131
132 // Insert initial annotations
133 initial := map[string]string{
134 "key1": "oldvalue1",
135 "key2": "oldvalue2",
136 "key3": "oldvalue3",
137 }
138
139 err := UpsertRepositoryAnnotations(db, "did:plc:charlie789", "updateapp", initial)
140 if err != nil {
141 t.Fatalf("Initial UpsertRepositoryAnnotations() error = %v", err)
142 }
143
144 // Update with new annotations (completely replaces old ones)
145 updated := map[string]string{
146 "key1": "newvalue1", // Updated
147 "key4": "newvalue4", // New key (key2 and key3 removed)
148 }
149
150 err = UpsertRepositoryAnnotations(db, "did:plc:charlie789", "updateapp", updated)
151 if err != nil {
152 t.Fatalf("Update UpsertRepositoryAnnotations() error = %v", err)
153 }
154
155 // Verify annotations were replaced
156 retrieved, err := GetRepositoryAnnotations(db, "did:plc:charlie789", "updateapp")
157 if err != nil {
158 t.Fatalf("GetRepositoryAnnotations() error = %v", err)
159 }
160
161 if len(retrieved) != len(updated) {
162 t.Errorf("Expected %d annotations, got %d", len(updated), len(retrieved))
163 }
164
165 // Verify new values
166 if retrieved["key1"] != "newvalue1" {
167 t.Errorf("key1 = %v, want newvalue1", retrieved["key1"])
168 }
169 if retrieved["key4"] != "newvalue4" {
170 t.Errorf("key4 = %v, want newvalue4", retrieved["key4"])
171 }
172
173 // Verify old keys were removed
174 if _, exists := retrieved["key2"]; exists {
175 t.Error("key2 should have been removed")
176 }
177 if _, exists := retrieved["key3"]; exists {
178 t.Error("key3 should have been removed")
179 }
180}
181
182// TestUpsertRepositoryAnnotations_EmptyMap tests upserting with empty map
183func TestUpsertRepositoryAnnotations_EmptyMap(t *testing.T) {
184 db := setupAnnotationsTestDB(t)
185 createAnnotationTestUser(t, db, "did:plc:dave111", "dave.bsky.social")
186
187 // Insert initial annotations
188 initial := map[string]string{
189 "key1": "value1",
190 "key2": "value2",
191 }
192
193 err := UpsertRepositoryAnnotations(db, "did:plc:dave111", "emptyapp", initial)
194 if err != nil {
195 t.Fatalf("Initial UpsertRepositoryAnnotations() error = %v", err)
196 }
197
198 // Upsert with empty map (should delete all)
199 empty := make(map[string]string)
200
201 err = UpsertRepositoryAnnotations(db, "did:plc:dave111", "emptyapp", empty)
202 if err != nil {
203 t.Fatalf("Empty UpsertRepositoryAnnotations() error = %v", err)
204 }
205
206 // Verify all annotations were deleted
207 retrieved, err := GetRepositoryAnnotations(db, "did:plc:dave111", "emptyapp")
208 if err != nil {
209 t.Fatalf("GetRepositoryAnnotations() error = %v", err)
210 }
211
212 if len(retrieved) != 0 {
213 t.Errorf("Expected 0 annotations after empty upsert, got %d", len(retrieved))
214 }
215}
216
217// TestUpsertRepositoryAnnotations_MultipleRepos tests isolation between repositories
218func TestUpsertRepositoryAnnotations_MultipleRepos(t *testing.T) {
219 db := setupAnnotationsTestDB(t)
220 createAnnotationTestUser(t, db, "did:plc:eve222", "eve.bsky.social")
221
222 // Insert annotations for repo1
223 repo1Annotations := map[string]string{
224 "repo": "repo1",
225 "key1": "value1",
226 }
227 err := UpsertRepositoryAnnotations(db, "did:plc:eve222", "repo1", repo1Annotations)
228 if err != nil {
229 t.Fatalf("UpsertRepositoryAnnotations(repo1) error = %v", err)
230 }
231
232 // Insert annotations for repo2 (same DID, different repo)
233 repo2Annotations := map[string]string{
234 "repo": "repo2",
235 "key2": "value2",
236 }
237 err = UpsertRepositoryAnnotations(db, "did:plc:eve222", "repo2", repo2Annotations)
238 if err != nil {
239 t.Fatalf("UpsertRepositoryAnnotations(repo2) error = %v", err)
240 }
241
242 // Verify repo1 annotations unchanged
243 retrieved1, err := GetRepositoryAnnotations(db, "did:plc:eve222", "repo1")
244 if err != nil {
245 t.Fatalf("GetRepositoryAnnotations(repo1) error = %v", err)
246 }
247 if len(retrieved1) != len(repo1Annotations) {
248 t.Errorf("repo1: Expected %d annotations, got %d", len(repo1Annotations), len(retrieved1))
249 }
250 if retrieved1["repo"] != "repo1" {
251 t.Errorf("repo1: Expected repo=repo1, got %v", retrieved1["repo"])
252 }
253
254 // Verify repo2 annotations
255 retrieved2, err := GetRepositoryAnnotations(db, "did:plc:eve222", "repo2")
256 if err != nil {
257 t.Fatalf("GetRepositoryAnnotations(repo2) error = %v", err)
258 }
259 if len(retrieved2) != len(repo2Annotations) {
260 t.Errorf("repo2: Expected %d annotations, got %d", len(repo2Annotations), len(retrieved2))
261 }
262 if retrieved2["repo"] != "repo2" {
263 t.Errorf("repo2: Expected repo=repo2, got %v", retrieved2["repo"])
264 }
265}
266
267// TestDeleteRepositoryAnnotations tests deleting annotations
268func TestDeleteRepositoryAnnotations(t *testing.T) {
269 db := setupAnnotationsTestDB(t)
270 createAnnotationTestUser(t, db, "did:plc:frank333", "frank.bsky.social")
271
272 // Insert annotations
273 annotations := map[string]string{
274 "key1": "value1",
275 "key2": "value2",
276 }
277 err := UpsertRepositoryAnnotations(db, "did:plc:frank333", "deleteapp", annotations)
278 if err != nil {
279 t.Fatalf("UpsertRepositoryAnnotations() error = %v", err)
280 }
281
282 // Verify annotations exist
283 retrieved, err := GetRepositoryAnnotations(db, "did:plc:frank333", "deleteapp")
284 if err != nil {
285 t.Fatalf("GetRepositoryAnnotations() error = %v", err)
286 }
287 if len(retrieved) != 2 {
288 t.Fatalf("Expected 2 annotations before delete, got %d", len(retrieved))
289 }
290
291 // Delete annotations
292 err = DeleteRepositoryAnnotations(db, "did:plc:frank333", "deleteapp")
293 if err != nil {
294 t.Fatalf("DeleteRepositoryAnnotations() error = %v", err)
295 }
296
297 // Verify annotations were deleted
298 retrieved, err = GetRepositoryAnnotations(db, "did:plc:frank333", "deleteapp")
299 if err != nil {
300 t.Fatalf("GetRepositoryAnnotations() after delete error = %v", err)
301 }
302 if len(retrieved) != 0 {
303 t.Errorf("Expected 0 annotations after delete, got %d", len(retrieved))
304 }
305}
306
307// TestDeleteRepositoryAnnotations_NonExistent tests deleting non-existent annotations
308func TestDeleteRepositoryAnnotations_NonExistent(t *testing.T) {
309 db := setupAnnotationsTestDB(t)
310
311 // Delete from non-existent repository (should not error)
312 err := DeleteRepositoryAnnotations(db, "did:plc:ghost999", "nonexistent")
313 if err != nil {
314 t.Errorf("DeleteRepositoryAnnotations() for non-existent repo should not error, got: %v", err)
315 }
316}
317
318// TestAnnotations_DifferentDIDs tests isolation between different DIDs
319func TestAnnotations_DifferentDIDs(t *testing.T) {
320 db := setupAnnotationsTestDB(t)
321 createAnnotationTestUser(t, db, "did:plc:alice123", "alice.bsky.social")
322 createAnnotationTestUser(t, db, "did:plc:bob456", "bob.bsky.social")
323
324 // Insert annotations for alice
325 aliceAnnotations := map[string]string{
326 "owner": "alice",
327 "key1": "alice-value1",
328 }
329 err := UpsertRepositoryAnnotations(db, "did:plc:alice123", "sharedname", aliceAnnotations)
330 if err != nil {
331 t.Fatalf("UpsertRepositoryAnnotations(alice) error = %v", err)
332 }
333
334 // Insert annotations for bob (same repo name, different DID)
335 bobAnnotations := map[string]string{
336 "owner": "bob",
337 "key1": "bob-value1",
338 }
339 err = UpsertRepositoryAnnotations(db, "did:plc:bob456", "sharedname", bobAnnotations)
340 if err != nil {
341 t.Fatalf("UpsertRepositoryAnnotations(bob) error = %v", err)
342 }
343
344 // Verify alice's annotations unchanged
345 aliceRetrieved, err := GetRepositoryAnnotations(db, "did:plc:alice123", "sharedname")
346 if err != nil {
347 t.Fatalf("GetRepositoryAnnotations(alice) error = %v", err)
348 }
349 if aliceRetrieved["owner"] != "alice" {
350 t.Errorf("alice: Expected owner=alice, got %v", aliceRetrieved["owner"])
351 }
352
353 // Verify bob's annotations
354 bobRetrieved, err := GetRepositoryAnnotations(db, "did:plc:bob456", "sharedname")
355 if err != nil {
356 t.Fatalf("GetRepositoryAnnotations(bob) error = %v", err)
357 }
358 if bobRetrieved["owner"] != "bob" {
359 t.Errorf("bob: Expected owner=bob, got %v", bobRetrieved["owner"])
360 }
361}