Skip to main content

max / makenotwork

6.4 KB · 195 lines History Blame Raw
1 //! DB-layer contract tests for `db/scanning.rs`: the quarantine purge, the
2 //! read-side status stamp, and their symmetry across every CDN-served image
3 //! surface.
4 //!
5 //! Audit A5 / B-Sto1: these functions had no direct coverage (only indirect
6 //! exercise through the end-to-end scan worker), and the purge silently omitted
7 //! the `projects` cover, so a quarantined project cover stayed rendered AND its
8 //! (public-bucket) object stayed un-reapable behind the `is_s3_key_live` guard,
9 //! while the symmetric stamp DID touch projects. These pin that the purge and
10 //! the stamp cover the SAME five surfaces, and that `quarantined_s3_keys`
11 //! filters by verdict.
12
13 use crate::harness::TestHarness;
14 use makenotwork::db::FileScanStatus;
15 use makenotwork::db::scanning;
16
17 /// Seed a project + item cover and one gallery image per parent, all pointing at
18 /// distinct known keys. Returns the (project_id, item_id) as strings.
19 async fn seed_cdn_image_surfaces(h: &mut TestHarness) -> (String, String) {
20 let setup = h.create_creator_with_item("scanlayer", "digital", 0).await;
21
22 // Item + project covers.
23 sqlx::query("UPDATE items SET cover_s3_key = 'k/itemcover', cover_image_url = 'https://cdn/x/itemcover' WHERE id = $1::uuid")
24 .bind(&setup.item_id)
25 .execute(&h.db)
26 .await
27 .unwrap();
28 sqlx::query("UPDATE projects SET cover_s3_key = 'k/projcover', cover_image_url = 'https://cdn/x/projcover' WHERE id = $1::uuid")
29 .bind(&setup.project_id)
30 .execute(&h.db)
31 .await
32 .unwrap();
33
34 // Gallery images (item_images / project_images).
35 sqlx::query(
36 "INSERT INTO item_images (item_id, s3_key, image_url, alt, position, file_size_bytes) \
37 VALUES ($1::uuid, 'k/itemimg', 'https://cdn/x/itemimg', '', 0, 10)",
38 )
39 .bind(&setup.item_id)
40 .execute(&h.db)
41 .await
42 .unwrap();
43 sqlx::query(
44 "INSERT INTO project_images (project_id, s3_key, image_url, alt, position, file_size_bytes) \
45 VALUES ($1::uuid, 'k/projimg', 'https://cdn/x/projimg', '', 0, 10)",
46 )
47 .bind(&setup.project_id)
48 .execute(&h.db)
49 .await
50 .unwrap();
51
52 (setup.project_id, setup.item_id)
53 }
54
55 /// Whether any live row still references `key` across the CDN image surfaces.
56 async fn key_still_referenced(h: &TestHarness, key: &str) -> bool {
57 let n: i64 = sqlx::query_scalar(
58 "SELECT \
59 (SELECT COUNT(*) FROM item_images WHERE s3_key = $1) \
60 + (SELECT COUNT(*) FROM project_images WHERE s3_key = $1) \
61 + (SELECT COUNT(*) FROM items WHERE cover_s3_key = $1) \
62 + (SELECT COUNT(*) FROM projects WHERE cover_s3_key = $1)",
63 )
64 .bind(key)
65 .fetch_one(&h.db)
66 .await
67 .unwrap();
68 n > 0
69 }
70
71 #[tokio::test]
72 async fn purge_clears_every_cdn_image_surface_including_project_cover() {
73 let mut h = TestHarness::new().await;
74 let _ = seed_cdn_image_surfaces(&mut h).await;
75
76 // Each surface's key must be fully de-referenced by the purge, otherwise the
77 // quarantined object stays rendered and un-reapable.
78 for key in ["k/itemcover", "k/projcover", "k/itemimg", "k/projimg"] {
79 assert!(
80 key_still_referenced(&h, key).await,
81 "precondition: {key} is referenced"
82 );
83 let removed = scanning::purge_cdn_image_rows_by_key(&h.db, key)
84 .await
85 .unwrap();
86 assert_eq!(
87 removed, 1,
88 "purge of {key} should clear exactly one surface"
89 );
90 assert!(
91 !key_still_referenced(&h, key).await,
92 "purge must fully de-reference {key}"
93 );
94 }
95 }
96
97 #[tokio::test]
98 async fn purge_keeps_the_item_and_project_rows() {
99 let mut h = TestHarness::new().await;
100 let (project_id, item_id) = seed_cdn_image_surfaces(&mut h).await;
101
102 scanning::purge_cdn_image_rows_by_key(&h.db, "k/itemcover")
103 .await
104 .unwrap();
105 scanning::purge_cdn_image_rows_by_key(&h.db, "k/projcover")
106 .await
107 .unwrap();
108
109 // Purging a quarantined cover must NULL the columns, never delete the row.
110 let item_exists: bool = sqlx::query_scalar(
111 "SELECT EXISTS(SELECT 1 FROM items WHERE id = $1::uuid AND cover_s3_key IS NULL)",
112 )
113 .bind(&item_id)
114 .fetch_one(&h.db)
115 .await
116 .unwrap();
117 assert!(
118 item_exists,
119 "item row must survive cover purge with a NULL cover"
120 );
121 let project_exists: bool = sqlx::query_scalar(
122 "SELECT EXISTS(SELECT 1 FROM projects WHERE id = $1::uuid AND cover_s3_key IS NULL)",
123 )
124 .bind(&project_id)
125 .fetch_one(&h.db)
126 .await
127 .unwrap();
128 assert!(
129 project_exists,
130 "project row must survive cover purge with a NULL cover"
131 );
132 }
133
134 #[tokio::test]
135 async fn stamp_and_purge_cover_the_same_surfaces() {
136 let mut h = TestHarness::new().await;
137 let _ = seed_cdn_image_surfaces(&mut h).await;
138
139 // The read-side stamp must touch each surface the purge touches (symmetry):
140 // one row affected per key, for all four.
141 for key in ["k/itemcover", "k/projcover", "k/itemimg", "k/projimg"] {
142 let stamped = scanning::set_cdn_image_scan_status_by_key(&h.db, key, FileScanStatus::Clean)
143 .await
144 .unwrap();
145 assert_eq!(
146 stamped, 1,
147 "stamp of {key} should affect exactly one surface"
148 );
149 }
150 }
151
152 #[tokio::test]
153 async fn quarantined_s3_keys_filters_by_verdict() {
154 let mut h = TestHarness::new().await;
155 let _ = seed_cdn_image_surfaces(&mut h).await;
156
157 // Two scan results: one quarantined, one clean.
158 for (key, status) in [("k/itemcover", "quarantined"), ("k/itemimg", "clean")] {
159 sqlx::query(
160 "INSERT INTO file_scan_results (s3_key, scan_status, scan_layers) \
161 VALUES ($1, $2, '[]'::jsonb)",
162 )
163 .bind(key)
164 .bind(status)
165 .execute(&h.db)
166 .await
167 .unwrap();
168 }
169
170 let quarantined = scanning::quarantined_s3_keys(
171 &h.db,
172 &[
173 "k/itemcover".to_string(),
174 "k/itemimg".to_string(),
175 "k/never-scanned".to_string(),
176 ],
177 )
178 .await
179 .unwrap();
180
181 assert!(
182 quarantined.contains("k/itemcover"),
183 "quarantined key must be returned"
184 );
185 assert!(
186 !quarantined.contains("k/itemimg"),
187 "clean key must not be returned"
188 );
189 assert!(
190 !quarantined.contains("k/never-scanned"),
191 "unscanned key must not be returned"
192 );
193 assert_eq!(quarantined.len(), 1);
194 }
195