Files
elmprodvpn/selective-vpn-api/app/transport_policy_idempotency_test.go

167 lines
6.7 KiB
Go

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)
}
}