package app import ( "encoding/json" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" ) func withTransportPolicyMutationTestPaths(t *testing.T) { t.Helper() tmp := t.TempDir() prevClients := transportClientsStatePath prevIfaces := transportInterfacesStatePath prevPolicy := transportPolicyStatePath prevPolicySnap := transportPolicySnapshotPath prevPlan := transportPolicyPlanStatePath prevRuntime := transportPolicyRuntimeStatePath prevRuntimeSnap := transportPolicyRuntimeSnapPath prevOwners := transportOwnershipStatePath prevConflicts := transportConflictsStatePath prevIdem := transportPolicyIdempotencyStatePath transportClientsStatePath = filepath.Join(tmp, "transport-clients.json") transportInterfacesStatePath = filepath.Join(tmp, "transport-interfaces.json") transportPolicyStatePath = filepath.Join(tmp, "transport-policies.json") transportPolicySnapshotPath = filepath.Join(tmp, "transport-policies.prev.json") transportPolicyPlanStatePath = filepath.Join(tmp, "transport-policies.plan.json") transportPolicyRuntimeStatePath = filepath.Join(tmp, "transport-policies.runtime.json") transportPolicyRuntimeSnapPath = filepath.Join(tmp, "transport-policies.runtime.prev.json") transportOwnershipStatePath = filepath.Join(tmp, "transport-ownership.json") transportConflictsStatePath = filepath.Join(tmp, "transport-conflicts.json") transportPolicyIdempotencyStatePath = filepath.Join(tmp, "transport-policy-idempotency.json") t.Cleanup(func() { transportClientsStatePath = prevClients transportInterfacesStatePath = prevIfaces transportPolicyStatePath = prevPolicy transportPolicySnapshotPath = prevPolicySnap transportPolicyPlanStatePath = prevPlan transportPolicyRuntimeStatePath = prevRuntime transportPolicyRuntimeSnapPath = prevRuntimeSnap transportOwnershipStatePath = prevOwners transportConflictsStatePath = prevConflicts transportPolicyIdempotencyStatePath = prevIdem }) } func TestTransportPolicyApplyIdempotencyReplay(t *testing.T) { withTransportPolicyMutationTestPaths(t) body := `{"base_revision":0,"intents":[]}` req1 := httptest.NewRequest(http.MethodPost, "/api/v1/transport/policies/apply", strings.NewReader(body)) req1.Header.Set("Idempotency-Key", "apply-replay-1") rec1 := httptest.NewRecorder() handleTransportPoliciesApplyExec(rec1, req1) if rec1.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rec1.Code, rec1.Body.String()) } var resp1 TransportPolicyResponse if err := json.Unmarshal(rec1.Body.Bytes(), &resp1); err != nil { t.Fatalf("decode first apply response: %v", err) } if !resp1.OK || resp1.PolicyRevision != 1 || strings.TrimSpace(resp1.ApplyID) == "" { t.Fatalf("unexpected first apply response: %#v", resp1) } req2 := httptest.NewRequest(http.MethodPost, "/api/v1/transport/policies/apply", strings.NewReader(body)) req2.Header.Set("Idempotency-Key", "apply-replay-1") rec2 := httptest.NewRecorder() handleTransportPoliciesApplyExec(rec2, req2) if rec2.Code != http.StatusOK { t.Fatalf("unexpected replay status: %d body=%s", rec2.Code, rec2.Body.String()) } var resp2 TransportPolicyResponse if err := json.Unmarshal(rec2.Body.Bytes(), &resp2); err != nil { t.Fatalf("decode replay apply response: %v", err) } if !resp2.OK || resp2.PolicyRevision != 1 || resp2.ApplyID != resp1.ApplyID { t.Fatalf("unexpected replay response: first=%#v second=%#v", resp1, resp2) } current := loadTransportPolicyState() if current.Revision != 1 { t.Fatalf("policy revision must remain 1 after replay, got %d", current.Revision) } idem := loadTransportPolicyIdempotencyState() if len(idem.Items) != 1 { t.Fatalf("expected one idempotency record, got %#v", idem) } } func TestTransportPolicyApplyIdempotencyRejectsPayloadReuse(t *testing.T) { withTransportPolicyMutationTestPaths(t) req1 := httptest.NewRequest(http.MethodPost, "/api/v1/transport/policies/apply", strings.NewReader(`{"base_revision":0,"intents":[]}`)) req1.Header.Set("Idempotency-Key", "apply-conflict-1") rec1 := httptest.NewRecorder() handleTransportPoliciesApplyExec(rec1, req1) req2 := httptest.NewRequest(http.MethodPost, "/api/v1/transport/policies/apply", strings.NewReader(`{"base_revision":1,"intents":[]}`)) req2.Header.Set("Idempotency-Key", "apply-conflict-1") rec2 := httptest.NewRecorder() handleTransportPoliciesApplyExec(rec2, req2) if rec2.Code != http.StatusOK { t.Fatalf("unexpected conflict status: %d body=%s", rec2.Code, rec2.Body.String()) } var resp2 TransportPolicyResponse if err := json.Unmarshal(rec2.Body.Bytes(), &resp2); err != nil { t.Fatalf("decode conflict apply response: %v", err) } if resp2.OK || resp2.Code != "IDEMPOTENCY_KEY_REUSED" { t.Fatalf("unexpected idempotency conflict response: %#v", resp2) } current := loadTransportPolicyState() if current.Revision != 1 { t.Fatalf("policy revision must remain 1 after conflicting replay, got %d", current.Revision) } } func TestTransportPolicyRollbackIdempotencyReplay(t *testing.T) { withTransportPolicyMutationTestPaths(t) applyReq := httptest.NewRequest(http.MethodPost, "/api/v1/transport/policies/apply", strings.NewReader(`{"base_revision":0,"intents":[]}`)) applyRec := httptest.NewRecorder() handleTransportPoliciesApplyExec(applyRec, applyReq) req1 := httptest.NewRequest(http.MethodPost, "/api/v1/transport/policies/rollback", strings.NewReader(`{"base_revision":1}`)) req1.Header.Set("Idempotency-Key", "rollback-replay-1") rec1 := httptest.NewRecorder() handleTransportPoliciesRollbackExec(rec1, req1) if rec1.Code != http.StatusOK { t.Fatalf("unexpected rollback status: %d body=%s", rec1.Code, rec1.Body.String()) } var resp1 TransportPolicyResponse if err := json.Unmarshal(rec1.Body.Bytes(), &resp1); err != nil { t.Fatalf("decode first rollback response: %v", err) } if !resp1.OK || resp1.PolicyRevision != 2 || strings.TrimSpace(resp1.ApplyID) == "" { t.Fatalf("unexpected first rollback response: %#v", resp1) } req2 := httptest.NewRequest(http.MethodPost, "/api/v1/transport/policies/rollback", strings.NewReader(`{"base_revision":1}`)) req2.Header.Set("Idempotency-Key", "rollback-replay-1") rec2 := httptest.NewRecorder() handleTransportPoliciesRollbackExec(rec2, req2) if rec2.Code != http.StatusOK { t.Fatalf("unexpected rollback replay status: %d body=%s", rec2.Code, rec2.Body.String()) } var resp2 TransportPolicyResponse if err := json.Unmarshal(rec2.Body.Bytes(), &resp2); err != nil { t.Fatalf("decode replay rollback response: %v", err) } if !resp2.OK || resp2.PolicyRevision != 2 || resp2.ApplyID != resp1.ApplyID { t.Fatalf("unexpected rollback replay response: first=%#v second=%#v", resp1, resp2) } current := loadTransportPolicyState() if current.Revision != 2 { t.Fatalf("policy revision must remain 2 after rollback replay, got %d", current.Revision) } }