@@ -178,6 +178,208 @@ func TestBackupFinal_ListRestores_ListFailed_503(t *testing.T) {
178178 require .Equal (t , http .StatusServiceUnavailable , resp .StatusCode )
179179}
180180
181+ // CreateRestore: GetTeamByID errors → team_lookup_failed (backup.go:411). In-
182+ // place restore (no target). resource(1) succeeds, team(2) errors. failAfter=1.
183+ func TestBackupFinal_CreateRestore_TeamLookup_503 (t * testing.T ) {
184+ seedDB , clean := testhelpers .SetupTestDB (t )
185+ defer clean ()
186+ rdb , cleanR := testhelpers .SetupTestRedis (t )
187+ defer cleanR ()
188+ teamID := testhelpers .MustCreateTeamDB (t , seedDB , "pro" )
189+ token := bkSeedPGResource (t , seedDB , teamID )
190+ var resID string
191+ require .NoError (t , seedDB .QueryRowContext (context .Background (),
192+ `SELECT id::text FROM resources WHERE token=$1::uuid` , token ).Scan (& resID ))
193+ backupID := seedBackupRow (t , seedDB , resID , "ok" )
194+
195+ faultDB := openFaultDB (t , 1 )
196+ h := handlers .NewBackupHandler (faultDB , rdb , plans .Default ())
197+ app := newBackupApp (t , h , teamID , uuid .NewString ())
198+ body := `{"backup_id":"` + backupID + `","destructive_acknowledgment":true}`
199+ resp := bkDo (t , app , http .MethodPost , "/api/v1/resources/" + token + "/restore" , body )
200+ defer resp .Body .Close ()
201+ require .Equal (t , http .StatusServiceUnavailable , resp .StatusCode )
202+ assert .Equal (t , "team_lookup_failed" , bkErr (t , resp ))
203+ }
204+
205+ // CreateRestore: GetBackupByIDForTeam errors → backup_lookup_failed
206+ // (backup.go:438). resource(1) + team(2) succeed, backup lookup(3) errors.
207+ // failAfter=2.
208+ func TestBackupFinal_CreateRestore_BackupLookup_503 (t * testing.T ) {
209+ seedDB , clean := testhelpers .SetupTestDB (t )
210+ defer clean ()
211+ rdb , cleanR := testhelpers .SetupTestRedis (t )
212+ defer cleanR ()
213+ teamID := testhelpers .MustCreateTeamDB (t , seedDB , "pro" )
214+ token := bkSeedPGResource (t , seedDB , teamID )
215+ var resID string
216+ require .NoError (t , seedDB .QueryRowContext (context .Background (),
217+ `SELECT id::text FROM resources WHERE token=$1::uuid` , token ).Scan (& resID ))
218+ backupID := seedBackupRow (t , seedDB , resID , "ok" )
219+
220+ faultDB := openFaultDB (t , 2 )
221+ h := handlers .NewBackupHandler (faultDB , rdb , plans .Default ())
222+ app := newBackupApp (t , h , teamID , uuid .NewString ())
223+ body := `{"backup_id":"` + backupID + `","destructive_acknowledgment":true}`
224+ resp := bkDo (t , app , http .MethodPost , "/api/v1/resources/" + token + "/restore" , body )
225+ defer resp .Body .Close ()
226+ require .Equal (t , http .StatusServiceUnavailable , resp .StatusCode )
227+ assert .Equal (t , "backup_lookup_failed" , bkErr (t , resp ))
228+ }
229+
230+ // CreateRestore: HasInflightRestore errors → inflight_check_failed
231+ // (backup.go:464). resource(1) + team(2) + backup(3) succeed, inflight(4)
232+ // errors. failAfter=3.
233+ func TestBackupFinal_CreateRestore_InflightCheck_503 (t * testing.T ) {
234+ seedDB , clean := testhelpers .SetupTestDB (t )
235+ defer clean ()
236+ rdb , cleanR := testhelpers .SetupTestRedis (t )
237+ defer cleanR ()
238+ teamID := testhelpers .MustCreateTeamDB (t , seedDB , "pro" )
239+ token := bkSeedPGResource (t , seedDB , teamID )
240+ var resID string
241+ require .NoError (t , seedDB .QueryRowContext (context .Background (),
242+ `SELECT id::text FROM resources WHERE token=$1::uuid` , token ).Scan (& resID ))
243+ backupID := seedBackupRow (t , seedDB , resID , "ok" )
244+
245+ faultDB := openFaultDB (t , 3 )
246+ h := handlers .NewBackupHandler (faultDB , rdb , plans .Default ())
247+ app := newBackupApp (t , h , teamID , uuid .NewString ())
248+ body := `{"backup_id":"` + backupID + `","destructive_acknowledgment":true}`
249+ resp := bkDo (t , app , http .MethodPost , "/api/v1/resources/" + token + "/restore" , body )
250+ defer resp .Body .Close ()
251+ require .Equal (t , http .StatusServiceUnavailable , resp .StatusCode )
252+ assert .Equal (t , "inflight_check_failed" , bkErr (t , resp ))
253+ }
254+
255+ // Bad team-id in Locals → unauthorized across CreateBackup / ListBackups /
256+ // CreateRestore / ListRestores (parseTeamID arms 104 / 224 / 320 / 550).
257+ func TestBackupFinal_BadTeamID_Unauthorized (t * testing.T ) {
258+ db , clean := testhelpers .SetupTestDB (t )
259+ defer clean ()
260+ rdb , cleanR := testhelpers .SetupTestRedis (t )
261+ defer cleanR ()
262+ h := handlers .NewBackupHandler (db , rdb , plans .Default ())
263+ app := newBackupApp (t , h , "not-a-uuid" , uuid .NewString ())
264+ tok := uuid .NewString ()
265+ for _ , route := range []struct {
266+ method , path string
267+ }{
268+ {http .MethodPost , "/api/v1/resources/" + tok + "/backup" },
269+ {http .MethodGet , "/api/v1/resources/" + tok + "/backups" },
270+ {http .MethodPost , "/api/v1/resources/" + tok + "/restore" },
271+ {http .MethodGet , "/api/v1/resources/" + tok + "/restores" },
272+ } {
273+ resp := bkDo (t , app , route .method , route .path , "" )
274+ assert .Equal (t , http .StatusUnauthorized , resp .StatusCode , "route %s %s" , route .method , route .path )
275+ resp .Body .Close ()
276+ }
277+ }
278+
279+ // Non-UUID :id → invalid_id across the four routes (224 / 230 / 332 / 557).
280+ func TestBackupFinal_BadResourceID_400 (t * testing.T ) {
281+ db , clean := testhelpers .SetupTestDB (t )
282+ defer clean ()
283+ rdb , cleanR := testhelpers .SetupTestRedis (t )
284+ defer cleanR ()
285+ teamID := testhelpers .MustCreateTeamDB (t , db , "pro" )
286+ h := handlers .NewBackupHandler (db , rdb , plans .Default ())
287+ app := newBackupApp (t , h , teamID , uuid .NewString ())
288+ for _ , route := range []struct { method , path string }{
289+ {http .MethodPost , "/api/v1/resources/not-a-uuid/backup" },
290+ {http .MethodGet , "/api/v1/resources/not-a-uuid/backups" },
291+ {http .MethodPost , "/api/v1/resources/not-a-uuid/restore" },
292+ {http .MethodGet , "/api/v1/resources/not-a-uuid/restores" },
293+ } {
294+ resp := bkDo (t , app , route .method , route .path , "" )
295+ assert .Equal (t , http .StatusBadRequest , resp .StatusCode , "route %s" , route .path )
296+ resp .Body .Close ()
297+ }
298+ }
299+
300+ // CreateRestore: in-place without destructive_acknowledgment → 400
301+ // destructive_ack_required (backup.go:404-area).
302+ func TestBackupFinal_CreateRestore_MissingAck_400 (t * testing.T ) {
303+ db , clean := testhelpers .SetupTestDB (t )
304+ defer clean ()
305+ rdb , cleanR := testhelpers .SetupTestRedis (t )
306+ defer cleanR ()
307+ teamID := testhelpers .MustCreateTeamDB (t , db , "pro" )
308+ token := bkSeedPGResource (t , db , teamID )
309+ var resID string
310+ require .NoError (t , db .QueryRowContext (context .Background (),
311+ `SELECT id::text FROM resources WHERE token=$1::uuid` , token ).Scan (& resID ))
312+ backupID := seedBackupRow (t , db , resID , "ok" )
313+
314+ h := handlers .NewBackupHandler (db , rdb , plans .Default ())
315+ app := newBackupApp (t , h , teamID , uuid .NewString ())
316+ body := `{"backup_id":"` + backupID + `"}` // no destructive_acknowledgment
317+ resp := bkDo (t , app , http .MethodPost , "/api/v1/resources/" + token + "/restore" , body )
318+ defer resp .Body .Close ()
319+ assert .Equal (t , http .StatusBadRequest , resp .StatusCode )
320+ assert .Equal (t , "destructive_ack_required" , bkErr (t , resp ))
321+ }
322+
323+ // CreateRestore: CreateRestoreRow errors → restore_create_failed (backup.go:508).
324+ // resource(1)+team(2)+backup(3)+inflight(4) succeed, the INSERT(5) errors.
325+ // failAfter=4.
326+ func TestBackupFinal_CreateRestore_InsertFailed_503 (t * testing.T ) {
327+ seedDB , clean := testhelpers .SetupTestDB (t )
328+ defer clean ()
329+ rdb , cleanR := testhelpers .SetupTestRedis (t )
330+ defer cleanR ()
331+ teamID := testhelpers .MustCreateTeamDB (t , seedDB , "pro" )
332+ token := bkSeedPGResource (t , seedDB , teamID )
333+ var resID string
334+ require .NoError (t , seedDB .QueryRowContext (context .Background (),
335+ `SELECT id::text FROM resources WHERE token=$1::uuid` , token ).Scan (& resID ))
336+ backupID := seedBackupRow (t , seedDB , resID , "ok" )
337+
338+ faultDB := openFaultDB (t , 4 )
339+ h := handlers .NewBackupHandler (faultDB , rdb , plans .Default ())
340+ app := newBackupApp (t , h , teamID , uuid .NewString ())
341+ body := `{"backup_id":"` + backupID + `","destructive_acknowledgment":true}`
342+ resp := bkDo (t , app , http .MethodPost , "/api/v1/resources/" + token + "/restore" , body )
343+ defer resp .Body .Close ()
344+ require .Equal (t , http .StatusServiceUnavailable , resp .StatusCode )
345+ assert .Equal (t , "restore_create_failed" , bkErr (t , resp ))
346+ }
347+
348+ // CreateRestore: missing user session → unauthorized (backup.go:325). The
349+ // newBackupApp helper pins a user; pass "" to drop it.
350+ func TestBackupFinal_CreateRestore_NoUser_401 (t * testing.T ) {
351+ db , clean := testhelpers .SetupTestDB (t )
352+ defer clean ()
353+ rdb , cleanR := testhelpers .SetupTestRedis (t )
354+ defer cleanR ()
355+ teamID := testhelpers .MustCreateTeamDB (t , db , "pro" )
356+ token := bkSeedPGResource (t , db , teamID )
357+ h := handlers .NewBackupHandler (db , rdb , plans .Default ())
358+ app := newBackupApp (t , h , teamID , "" ) // no user-id local
359+ body := `{"backup_id":"` + uuid .NewString () + `","destructive_acknowledgment":true}`
360+ resp := bkDo (t , app , http .MethodPost , "/api/v1/resources/" + token + "/restore" , body )
361+ defer resp .Body .Close ()
362+ assert .Equal (t , http .StatusUnauthorized , resp .StatusCode )
363+ }
364+
365+ // ListRestores: COUNT fails after list succeeds → 200 with total=len(items)
366+ // (backup.go:579). resource(1)+list(2) succeed, count(3) errors. failAfter=2.
367+ func TestBackupFinal_ListRestores_CountFail_200 (t * testing.T ) {
368+ seedDB , clean := testhelpers .SetupTestDB (t )
369+ defer clean ()
370+ rdb , cleanR := testhelpers .SetupTestRedis (t )
371+ defer cleanR ()
372+ teamID := testhelpers .MustCreateTeamDB (t , seedDB , "pro" )
373+ token := bkSeedPGResource (t , seedDB , teamID )
374+
375+ faultDB := openFaultDB (t , 2 )
376+ h := handlers .NewBackupHandler (faultDB , rdb , plans .Default ())
377+ app := newBackupApp (t , h , teamID , uuid .NewString ())
378+ resp := bkDo (t , app , http .MethodGet , "/api/v1/resources/" + token + "/restores" , "" )
379+ defer resp .Body .Close ()
380+ require .Equal (t , http .StatusOK , resp .StatusCode )
381+ }
382+
181383// decodeJSON reads the response body into v.
182384func decodeJSON (resp * http.Response , v any ) error {
183385 return json .NewDecoder (resp .Body ).Decode (v )
0 commit comments