Skip to content

Commit fce99e3

Browse files
Merge branch 'master' into fix/buildlogcache-size-cap-2026-06-04
2 parents 0a54a29 + 893bed9 commit fce99e3

2 files changed

Lines changed: 33 additions & 6 deletions

File tree

internal/handlers/coverage_resource_files_test.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ import (
2929
// session for the supplied tier. Mirrors setupPauseFixture but exposes the
3030
// app + db + jwt + teamID for arbitrary cross-cutting tests.
3131
type authedFixture struct {
32-
app interface {
32+
app interface {
3333
Test(req *http.Request, msTimeout ...int) (*http.Response, error)
3434
}
35-
db *sql.DB
36-
jwt string
37-
teamID string
38-
userID string
39-
teamUUID uuid.UUID
35+
db *sql.DB
36+
jwt string
37+
teamID string
38+
userID string
39+
teamUUID uuid.UUID
4040
}
4141

4242
func setupAuthedFixture(t *testing.T, planTier string) authedFixture {
@@ -1486,3 +1486,22 @@ func TestResourceList_AllFieldsPresent(t *testing.T) {
14861486
// ───────────────────────────────────────────────────────────────────────────
14871487
var _ = fmt.Sprintf
14881488
var _ = uuid.Nil
1489+
1490+
// TestResourceDelete_Idempotent_DoubleDelete — bug-bash #4/#12: a repeated
1491+
// DELETE on an already-deleted resource returns 200 already_deleted and does NOT
1492+
// re-run the soft-delete + backend deprovision.
1493+
func TestResourceDelete_Idempotent_DoubleDelete(t *testing.T) {
1494+
fix := setupAuthedFixture(t, "hobby")
1495+
_, tok := insertResourceCov(t, fix.db, fix.teamID, "redis", "hobby")
1496+
1497+
r1 := authedDelete(t, fix, "/api/v1/resources/"+tok)
1498+
_ = r1.Body.Close()
1499+
require.Equal(t, http.StatusOK, r1.StatusCode)
1500+
1501+
r2 := authedDelete(t, fix, "/api/v1/resources/"+tok)
1502+
defer r2.Body.Close()
1503+
require.Equal(t, http.StatusOK, r2.StatusCode, "second DELETE must be idempotent 200")
1504+
var body map[string]any
1505+
require.NoError(t, json.NewDecoder(r2.Body).Decode(&body))
1506+
assert.Equal(t, true, body["already_deleted"], "repeat DELETE must report already_deleted")
1507+
}

internal/handlers/resource.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ func (h *ResourceHandler) Delete(c *fiber.Ctx) error {
216216
return respondError(c, fiber.StatusNotFound, "not_found", "Resource not found")
217217
}
218218

219+
// Idempotent DELETE (bug-bash #4/#12): a repeated DELETE (retry, double-click,
220+
// concurrent call) must NOT re-soft-delete and, worse, re-deprovision the
221+
// backend a second time. The first DELETE already tore it down — report
222+
// success and do nothing.
223+
if resource.Status == "deleted" {
224+
return c.JSON(fiber.Map{"ok": true, "already_deleted": true, "id": resource.ID.String()})
225+
}
226+
219227
if err := models.SoftDeleteResource(c.Context(), h.db, resource.ID); err != nil {
220228
slog.Error("resource.delete.failed",
221229
"error", err,

0 commit comments

Comments
 (0)