From 89611164e32231892e08fd288b55557c2a307b31 Mon Sep 17 00:00:00 2001 From: Mathias Date: Sat, 3 Oct 2026 21:06:22 +0200 Subject: [PATCH] test(web): per-recording delete is owner-scoped, refused while active, offered only when finished (#37), red Co-Authored-By: Claude Sonnet 5.5 --- internal/web/uploads_delete_handler_test.go | 85 +++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 internal/web/uploads_delete_handler_test.go diff --git a/internal/web/uploads_delete_handler_test.go b/internal/web/uploads_delete_handler_test.go new file mode 100644 index 0000000..2d62e62 --- /dev/null +++ b/internal/web/uploads_delete_handler_test.go @@ -0,0 +1,85 @@ +package web_test + +// Deleting one recording (#37). Owner-scoped like every other upload route: another user's +// id is a 404 and the row survives; an upload still being processed is refused. + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func deleteReq(id string) *http.Request { + return httptest.NewRequest(http.MethodPost, "/uploads/"+id+"/delete", nil) +} + +func uploadExists(t *testing.T, id string) bool { + t.Helper() + var n int + require.NoError(t, rawPool(t).QueryRow(context.Background(), `SELECT count(*) FROM uploaded_transcripts WHERE id = $1`, id).Scan(&n)) + return n == 1 +} + +func TestUploadDelete_AnOwnerRemovesAFinishedOrFailedRecordingAndLandsOnTheList(t *testing.T) { + for _, status := range []string{"done", "failed"} { + t.Run(status, func(t *testing.T) { + app, _ := newUploadApp(t, audioOK, 200<<20) + id := seedUpload(t, userID, "Roadmap sync", status, "transcription_failed") + + rec := do(t, app, deleteReq(id)) + + assert.Equal(t, http.StatusSeeOther, rec.Code) + assert.Equal(t, "/uploads", rec.Header().Get("Location")) + assert.False(t, uploadExists(t, id)) + }) + } +} + +func TestUploadDelete_AnotherUsersRecordingIs404AndSurvives(t *testing.T) { + app, _ := newUploadApp(t, audioOK, 200<<20) + theirs := seedUpload(t, otherUserID, "Their private meeting", "done", meetingText) + + rec := do(t, app, deleteReq(theirs)) + + assert.Equal(t, http.StatusNotFound, rec.Code) + assert.True(t, uploadExists(t, theirs), "a non-owner's delete must not remove the row") +} + +func TestUploadDelete_ARecordingStillBeingTranscribedIsRefusedWith409AndKept(t *testing.T) { + for _, status := range []string{"queued", "transcribing"} { + t.Run(status, func(t *testing.T) { + app, _ := newUploadApp(t, audioOK, 200<<20) + id := seedUpload(t, userID, "m", status, "") + + rec := do(t, app, deleteReq(id)) + + assert.Equal(t, http.StatusConflict, rec.Code) + assert.Contains(t, body(t, rec), "still being transcribed") + assert.True(t, uploadExists(t, id)) + }) + } +} + +func TestUploadDelete_ANonIDIsA404NotAServerError(t *testing.T) { + app, _ := newUploadApp(t, audioOK, 200<<20) + + assert.Equal(t, http.StatusNotFound, do(t, app, deleteReq("not-a-uuid")).Code) +} + +func TestUploadPage_OffersDeleteOnlyForAFinishedOrFailedRecording(t *testing.T) { + for status, want := range map[string]bool{"done": true, "failed": true, "queued": false, "transcribing": false} { + t.Run(status, func(t *testing.T) { + app, _ := newUploadApp(t, audioOK, 200<<20) + id := seedUpload(t, userID, "m", status, "transcription_failed") + + b := body(t, do(t, app, get("/uploads/"+id))) + + assert.Equal(t, want, strings.Contains(b, `action="/uploads/`+id+`/delete"`)) + }) + } +}