package app import ( "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestHandleTransportOwnershipRebuildsOnPlanDigestDrift(t *testing.T) { withTransportPolicyMutationTestPaths(t) policy := TransportPolicyState{ Version: transportStateVersion, Revision: 17, Intents: []TransportPolicyIntent{ {SelectorType: "domain", SelectorValue: "demo.invalid", ClientID: "sg-a", Priority: 100, Mode: "strict"}, }, } if err := saveTransportPolicyState(policy); err != nil { t.Fatalf("save policy: %v", err) } clients := transportClientsState{ Version: transportStateVersion, Items: []TransportClient{ { ID: "sg-a", Kind: TransportClientSingBox, IfaceID: "shared", RoutingTable: "agvpn_sg_a", MarkHex: "0x110", PriorityBase: 13250, Enabled: true, Status: TransportClientUp, }, }, } if err := saveTransportClientsState(clients); err != nil { t.Fatalf("save clients: %v", err) } plan, conflicts := compileTransportPolicyPlan(policy.Intents, clients.Items, policy.Revision) if len(conflicts) > 0 { t.Fatalf("unexpected compile conflicts: %#v", conflicts) } if err := saveTransportPolicyCompilePlan(plan); err != nil { t.Fatalf("save plan: %v", err) } legacyOwners := TransportOwnershipState{ Version: transportStateVersion, PolicyRevision: policy.Revision, PlanDigest: "legacy-digest", Items: []TransportOwnershipRecord{ { Key: "domain:demo.invalid", SelectorType: "domain", SelectorValue: "demo.invalid", ClientID: "sg-a", IfaceID: "shared", }, }, } if err := saveTransportOwnershipState(legacyOwners); err != nil { t.Fatalf("save owners: %v", err) } req := httptest.NewRequest(http.MethodGet, "/api/v1/transport/owners", nil) rec := httptest.NewRecorder() handleTransportOwnership(rec, req) if rec.Code != http.StatusOK { t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) } var resp TransportOwnershipResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("decode response: %v", err) } expectedDigest := digestTransportPolicyCompilePlan(plan) if resp.PlanDigest != expectedDigest { t.Fatalf("unexpected response plan_digest: got=%q want=%q", resp.PlanDigest, expectedDigest) } if len(resp.Items) != 1 { t.Fatalf("unexpected ownership response items: %d", len(resp.Items)) } if resp.Items[0].OwnerScope == "" { t.Fatalf("expected owner_scope in response item: %#v", resp.Items[0]) } rebuilt := loadTransportOwnershipState() if rebuilt.PlanDigest != expectedDigest { t.Fatalf("ownership state plan_digest not rebuilt: got=%q want=%q", rebuilt.PlanDigest, expectedDigest) } if len(rebuilt.Items) != 1 || rebuilt.Items[0].OwnerScope == "" { t.Fatalf("ownership state did not rebuild owner_scope: %#v", rebuilt.Items) } }