|
| 1 | +package router_test |
| 2 | + |
| 3 | +// resource_delete_env_lookup_test.go — drives the DELETE /api/v1/resources/:id |
| 4 | +// route through the REAL router so the WithEnvLookup closure wired in |
| 5 | +// router.go (the `return handlers.ResourceEnvByTokenOrIDForMiddleware(c, db)` |
| 6 | +// line) executes under unit coverage, not just E2E. Wave-2 A1 renamed that |
| 7 | +// helper (token-or-id resolution); this test pins the wiring: |
| 8 | +// |
| 9 | +// - an authenticated DELETE addressed by the resource's ROW ID resolves |
| 10 | +// through the env-lookup (id fallback) and soft-deletes the row, and |
| 11 | +// - a second DELETE on the same (now-deleted) row reports idempotent |
| 12 | +// success — proving the closure's fail-open path also routes through. |
| 13 | + |
| 14 | +import ( |
| 15 | + "context" |
| 16 | + "net/http" |
| 17 | + "net/http/httptest" |
| 18 | + "testing" |
| 19 | + |
| 20 | + "github.com/stretchr/testify/require" |
| 21 | + |
| 22 | + "instant.dev/internal/email" |
| 23 | + "instant.dev/internal/plans" |
| 24 | + "instant.dev/internal/router" |
| 25 | + "instant.dev/internal/testhelpers" |
| 26 | +) |
| 27 | + |
| 28 | +func TestRouter_ResourceDelete_ByRowID_EnvLookupWired(t *testing.T) { |
| 29 | + db, dbClean := testhelpers.SetupTestDB(t) |
| 30 | + defer dbClean() |
| 31 | + rdb, rdbClean := testhelpers.SetupTestRedis(t) |
| 32 | + defer rdbClean() |
| 33 | + |
| 34 | + cfg := newRouterTestConfig() |
| 35 | + app := router.New(cfg, db, rdb, nil, email.NewNoop(), plans.Default(), nil, nil) |
| 36 | + |
| 37 | + // Seed a team + user + an active resource owned by the team. |
| 38 | + teamID := testhelpers.MustCreateTeamDB(t, db, "pro") |
| 39 | + var userID string |
| 40 | + require.NoError(t, db.QueryRowContext(context.Background(), |
| 41 | + `INSERT INTO users (team_id, email) VALUES ($1::uuid, $2) RETURNING id::text`, |
| 42 | + teamID, testhelpers.UniqueEmail(t)).Scan(&userID)) |
| 43 | + jwt := testhelpers.MustSignSessionJWT(t, userID, teamID, "router-del@example.com") |
| 44 | + |
| 45 | + var rowID string |
| 46 | + require.NoError(t, db.QueryRowContext(context.Background(), |
| 47 | + `INSERT INTO resources (team_id, resource_type, tier, env, status) |
| 48 | + VALUES ($1::uuid, 'webhook', 'pro', 'staging', 'active') |
| 49 | + RETURNING id::text`, teamID).Scan(&rowID)) |
| 50 | + |
| 51 | + doDelete := func() *http.Response { |
| 52 | + req := httptest.NewRequest(http.MethodDelete, "/api/v1/resources/"+rowID, nil) |
| 53 | + req.Header.Set("Authorization", "Bearer "+jwt) |
| 54 | + resp, err := app.Test(req, 15000) |
| 55 | + require.NoError(t, err) |
| 56 | + return resp |
| 57 | + } |
| 58 | + |
| 59 | + // DELETE by ROW ID through the real router: env-policy middleware runs |
| 60 | + // the token-or-id env lookup, then the handler resolves + deletes. |
| 61 | + resp := doDelete() |
| 62 | + defer resp.Body.Close() |
| 63 | + require.Equal(t, http.StatusOK, resp.StatusCode, |
| 64 | + "DELETE by row id through the real router must succeed") |
| 65 | + |
| 66 | + var status string |
| 67 | + require.NoError(t, db.QueryRowContext(context.Background(), |
| 68 | + `SELECT status FROM resources WHERE id = $1::uuid`, rowID).Scan(&status)) |
| 69 | + require.Equal(t, "deleted", status) |
| 70 | + |
| 71 | + // Idempotent retry: the row is now 'deleted'; the env lookup still runs |
| 72 | + // (resolves the deleted row) and the handler reports already_deleted. |
| 73 | + resp2 := doDelete() |
| 74 | + defer resp2.Body.Close() |
| 75 | + require.Equal(t, http.StatusOK, resp2.StatusCode, |
| 76 | + "repeat DELETE must be idempotent success") |
| 77 | +} |
0 commit comments