@@ -140,6 +140,90 @@ describe('DELETE /admin/entries/:id removes its R2 image', () => {
140140 } ) ;
141141} ) ;
142142
143+ describe ( 'POST /admin/menus/:menuId/image' , ( ) => {
144+ it ( 'puts to R2 under images/menus/ and stores icon_url' , async ( ) => {
145+ const r2 = makeR2 ( ) ;
146+ const { db, env, headers } = await adminEnv ( r2 ) ;
147+ seedMenu ( db , 'm-1' , 'food' ) ;
148+
149+ const res = await testRequest ( '/admin/menus/m-1/image' , {
150+ method : 'POST' , headers, env, body : jpegBody ( ) ,
151+ } ) ;
152+ expect ( res . status ) . toBe ( 200 ) ;
153+ const body = await res . json ( ) as { ok : boolean ; iconUrl : string } ;
154+ expect ( body . ok ) . toBe ( true ) ;
155+ expect ( r2 . calls . put . some ( k => k . startsWith ( 'images/menus/m-1-' ) ) ) . toBe ( true ) ;
156+ expect ( body . iconUrl . startsWith ( `${ R2_BASE } /images/menus/m-1-` ) ) . toBe ( true ) ;
157+
158+ const row = db . raw . prepare ( 'SELECT icon_url FROM menus WHERE id = ?' ) . get ( 'm-1' ) as { icon_url : string } ;
159+ expect ( row . icon_url ) . toBe ( body . iconUrl ) ;
160+ } ) ;
161+
162+ it ( 'prunes the previous R2 object on replace' , async ( ) => {
163+ const r2 = makeR2 ( ) ;
164+ const { db, env, headers } = await adminEnv ( r2 ) ;
165+ seedMenu ( db , 'm-1' , 'food' ) ;
166+ const oldKey = 'images/menus/m-1-111.jpg' ;
167+ db . raw . prepare ( 'UPDATE menus SET icon_url = ? WHERE id = ?' ) . run ( `${ R2_BASE } /${ oldKey } ` , 'm-1' ) ;
168+
169+ const res = await testRequest ( '/admin/menus/m-1/image' , {
170+ method : 'POST' , headers, env, body : jpegBody ( ) ,
171+ } ) ;
172+ expect ( res . status ) . toBe ( 200 ) ;
173+ expect ( r2 . calls . delete ) . toContain ( oldKey ) ;
174+ } ) ;
175+
176+ it ( 'returns 404 for a missing menu' , async ( ) => {
177+ const r2 = makeR2 ( ) ;
178+ const { env, headers } = await adminEnv ( r2 ) ;
179+ const res = await testRequest ( '/admin/menus/nope/image' , {
180+ method : 'POST' , headers, env, body : jpegBody ( ) ,
181+ } ) ;
182+ expect ( res . status ) . toBe ( 404 ) ;
183+ } ) ;
184+ } ) ;
185+
186+ describe ( 'DELETE /admin/menus/:menuId/image' , ( ) => {
187+ it ( 'prunes the R2 object and nulls icon_url' , async ( ) => {
188+ const r2 = makeR2 ( ) ;
189+ const { db, env, headers } = await adminEnv ( r2 ) ;
190+ seedMenu ( db , 'm-1' , 'food' ) ;
191+ const key = 'images/menus/m-1-123.jpg' ;
192+ db . raw . prepare ( 'UPDATE menus SET icon_url = ? WHERE id = ?' ) . run ( `${ R2_BASE } /${ key } ` , 'm-1' ) ;
193+
194+ const res = await testRequest ( '/admin/menus/m-1/image' , { method : 'DELETE' , headers, env } ) ;
195+ expect ( res . status ) . toBe ( 200 ) ;
196+ expect ( r2 . calls . delete ) . toContain ( key ) ;
197+ const row = db . raw . prepare ( 'SELECT icon_url FROM menus WHERE id = ?' ) . get ( 'm-1' ) as { icon_url : string | null } ;
198+ expect ( row . icon_url ) . toBeNull ( ) ;
199+ } ) ;
200+
201+ it ( 'returns 404 for a missing menu' , async ( ) => {
202+ const r2 = makeR2 ( ) ;
203+ const { env, headers } = await adminEnv ( r2 ) ;
204+ const res = await testRequest ( '/admin/menus/nope/image' , { method : 'DELETE' , headers, env } ) ;
205+ expect ( res . status ) . toBe ( 404 ) ;
206+ } ) ;
207+ } ) ;
208+
209+ describe ( 'menu iconUrl exposure' , ( ) => {
210+ it ( 'GET /admin/menus and /catalog include iconUrl' , async ( ) => {
211+ const { db, env, headers } = await adminEnv ( ) ;
212+ seedMenu ( db , 'm-1' , 'food' ) ;
213+ db . raw . prepare ( 'UPDATE menus SET icon_url = ? WHERE id = ?' ) . run ( `${ R2_BASE } /images/menus/m-1-1.jpg` , 'm-1' ) ;
214+
215+ const adminRes = await testRequest ( '/admin/menus' , { headers, env } ) ;
216+ expect ( adminRes . status ) . toBe ( 200 ) ;
217+ const adminBody = await adminRes . json ( ) as { menus : Array < { id : string ; iconUrl : string | null } > } ;
218+ expect ( adminBody . menus [ 0 ] . iconUrl ) . toBe ( `${ R2_BASE } /images/menus/m-1-1.jpg` ) ;
219+
220+ const catRes = await testRequest ( '/catalog' , { env } ) ;
221+ expect ( catRes . status ) . toBe ( 200 ) ;
222+ const catBody = await catRes . json ( ) as { menus : Array < { id : string ; iconUrl : string | null } > } ;
223+ expect ( catBody . menus [ 0 ] . iconUrl ) . toBe ( `${ R2_BASE } /images/menus/m-1-1.jpg` ) ;
224+ } ) ;
225+ } ) ;
226+
143227describe ( 'DELETE /admin/menus/:menuId FK cascade' , ( ) => {
144228 it ( 'removes the menu and its memberships, keeps the entry' , async ( ) => {
145229 const { db, env, headers } = await adminEnv ( ) ;
0 commit comments