Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions internal/handlers/coverage_resource_files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ import (
// session for the supplied tier. Mirrors setupPauseFixture but exposes the
// app + db + jwt + teamID for arbitrary cross-cutting tests.
type authedFixture struct {
app interface {
app interface {
Test(req *http.Request, msTimeout ...int) (*http.Response, error)
}
db *sql.DB
jwt string
teamID string
userID string
teamUUID uuid.UUID
db *sql.DB
jwt string
teamID string
userID string
teamUUID uuid.UUID
}

func setupAuthedFixture(t *testing.T, planTier string) authedFixture {
Expand Down Expand Up @@ -1486,3 +1486,22 @@ func TestResourceList_AllFieldsPresent(t *testing.T) {
// ───────────────────────────────────────────────────────────────────────────
var _ = fmt.Sprintf
var _ = uuid.Nil

// TestResourceDelete_Idempotent_DoubleDelete — bug-bash #4/#12: a repeated
// DELETE on an already-deleted resource returns 200 already_deleted and does NOT
// re-run the soft-delete + backend deprovision.
func TestResourceDelete_Idempotent_DoubleDelete(t *testing.T) {
fix := setupAuthedFixture(t, "hobby")
_, tok := insertResourceCov(t, fix.db, fix.teamID, "redis", "hobby")

r1 := authedDelete(t, fix, "/api/v1/resources/"+tok)
_ = r1.Body.Close()
require.Equal(t, http.StatusOK, r1.StatusCode)

r2 := authedDelete(t, fix, "/api/v1/resources/"+tok)
defer r2.Body.Close()
require.Equal(t, http.StatusOK, r2.StatusCode, "second DELETE must be idempotent 200")
var body map[string]any
require.NoError(t, json.NewDecoder(r2.Body).Decode(&body))
assert.Equal(t, true, body["already_deleted"], "repeat DELETE must report already_deleted")
}
8 changes: 8 additions & 0 deletions internal/handlers/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ func (h *ResourceHandler) Delete(c *fiber.Ctx) error {
return respondError(c, fiber.StatusNotFound, "not_found", "Resource not found")
}

// Idempotent DELETE (bug-bash #4/#12): a repeated DELETE (retry, double-click,
// concurrent call) must NOT re-soft-delete and, worse, re-deprovision the
// backend a second time. The first DELETE already tore it down — report
// success and do nothing.
if resource.Status == "deleted" {
return c.JSON(fiber.Map{"ok": true, "already_deleted": true, "id": resource.ID.String()})
}

if err := models.SoftDeleteResource(c.Context(), h.db, resource.ID); err != nil {
slog.Error("resource.delete.failed",
"error", err,
Expand Down
Loading