Refactor RDP proxy handling and update related tests
This commit is contained in:
@@ -347,6 +347,71 @@ func TestAssignNodeRoleRejectsUnknownRole(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignNodeRoleAllowsWebAdminPlacementRoles(t *testing.T) {
|
||||
roles := []string{
|
||||
"public-ingress",
|
||||
"admin-ingress",
|
||||
"global-admin-runtime",
|
||||
"cluster-admin-runtime",
|
||||
"organization-portal-runtime",
|
||||
"user-portal-runtime",
|
||||
"identity-runtime",
|
||||
"policy-authority",
|
||||
"audit-sink",
|
||||
}
|
||||
for _, role := range roles {
|
||||
t.Run(role, func(t *testing.T) {
|
||||
store := &fakeRepository{platformRole: PlatformRoleAdmin}
|
||||
service := NewService(store)
|
||||
|
||||
item, err := service.AssignNodeRole(context.Background(), AssignNodeRoleInput{
|
||||
ActorUserID: "admin-1",
|
||||
ClusterID: "cluster-1",
|
||||
NodeID: "node-1",
|
||||
Role: role,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("assign role: %v", err)
|
||||
}
|
||||
if item.Role != role {
|
||||
t.Fatalf("role = %q, want %q", item.Role, role)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFabricAdminServiceClassesAreScopedToAdminRoles(t *testing.T) {
|
||||
cases := []struct {
|
||||
serviceClass string
|
||||
requiredRole string
|
||||
pathNeedle string
|
||||
}{
|
||||
{FabricServiceClassPlatformAdmin, "global-admin-runtime", "platform-admin"},
|
||||
{FabricServiceClassClusterAdmin, "cluster-admin-runtime", "cluster-admin"},
|
||||
{FabricServiceClassOrganization, "organization-portal-runtime", "organizations"},
|
||||
{FabricServiceClassUserPortal, "user-portal-runtime", "users"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.serviceClass, func(t *testing.T) {
|
||||
if !isAllowedFabricServiceClass(tc.serviceClass) {
|
||||
t.Fatalf("service class %q is not allowed", tc.serviceClass)
|
||||
}
|
||||
roles := normalizeFabricRequiredRoles(nil, tc.serviceClass)
|
||||
if !containsString(roles, tc.requiredRole) || !containsString(roles, "identity-runtime") || !containsString(roles, "policy-authority") {
|
||||
t.Fatalf("required roles = %+v", roles)
|
||||
}
|
||||
channels := normalizeFabricServiceChannels(nil, tc.serviceClass)
|
||||
if !containsString(channels, FabricChannelControl) || !containsString(channels, FabricChannelInteractive) || !containsString(channels, FabricChannelReliable) {
|
||||
t.Fatalf("channels = %+v", channels)
|
||||
}
|
||||
ingress := fabricServiceChannelHTTPIngress(tc.serviceClass)
|
||||
if !strings.Contains(ingress.PathTemplate, tc.pathNeedle) {
|
||||
t.Fatalf("path = %q, want %q", ingress.PathTemplate, tc.pathNeedle)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachExistingNodeRequiresPlatformAdmin(t *testing.T) {
|
||||
store := &fakeRepository{platformRole: "user"}
|
||||
service := NewService(store)
|
||||
@@ -567,6 +632,70 @@ func TestApproveJoinRequestReturnsBootstrapContract(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApproveJoinRequestReturnsSignedQuorumDescriptor(t *testing.T) {
|
||||
keys, err := clusterauth.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("generate key: %v", err)
|
||||
}
|
||||
quorum := &QuorumDescriptor{
|
||||
SchemaVersion: clusterauth.QuorumSchemaVersion,
|
||||
ClusterID: "cluster-1",
|
||||
Epoch: "epoch-1",
|
||||
Threshold: 1,
|
||||
Members: []clusterauth.QuorumMember{
|
||||
{
|
||||
NodeID: "authority-1",
|
||||
Role: "update-authority",
|
||||
PublicKey: keys.PublicKeyB64,
|
||||
PublicKeyFingerprint: keys.Fingerprint,
|
||||
Scopes: []string{"update-authority"},
|
||||
},
|
||||
},
|
||||
}
|
||||
store := &fakeRepository{
|
||||
platformRole: PlatformRoleAdmin,
|
||||
clusterAuthority: ClusterAuthorityKey{
|
||||
ClusterAuthorityDescriptor: ClusterAuthorityDescriptor{
|
||||
SchemaVersion: clusterauth.AuthoritySchemaVersion,
|
||||
ClusterID: "cluster-1",
|
||||
AuthorityState: "active",
|
||||
KeyAlgorithm: clusterauth.AlgorithmEd25519,
|
||||
PublicKey: keys.PublicKeyB64,
|
||||
PublicKeyFingerprint: keys.Fingerprint,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
},
|
||||
PrivateKey: keys.PrivateKeyB64,
|
||||
QuorumDescriptor: quorum,
|
||||
},
|
||||
}
|
||||
service := NewService(store)
|
||||
|
||||
approved, err := service.ApproveJoinRequest(context.Background(), ApproveJoinRequestInput{
|
||||
ActorUserID: "admin-1",
|
||||
ClusterID: "cluster-1",
|
||||
JoinRequestID: "join-request-1",
|
||||
NodeKey: "node-key-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("approve join request: %v", err)
|
||||
}
|
||||
if approved.Bootstrap.ClusterAuthorityQuorum == nil {
|
||||
t.Fatalf("bootstrap missing quorum descriptor: %+v", approved.Bootstrap)
|
||||
}
|
||||
var payload clusterNodeApprovalAuthorityPayload
|
||||
if err := json.Unmarshal(approved.Bootstrap.AuthorityPayload, &payload); err != nil {
|
||||
t.Fatalf("decode authority payload: %v", err)
|
||||
}
|
||||
quorumHash, err := clusterauth.QuorumDescriptorHash(*quorum)
|
||||
if err != nil {
|
||||
t.Fatalf("hash quorum: %v", err)
|
||||
}
|
||||
if payload.ClusterAuthorityQuorumSHA256 != quorumHash {
|
||||
t.Fatalf("quorum hash = %q, want %q", payload.ClusterAuthorityQuorumSHA256, quorumHash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetJoinRequestBootstrapReturnsSignedApproval(t *testing.T) {
|
||||
nodeID := "node-1"
|
||||
store := &fakeRepository{
|
||||
@@ -694,7 +823,8 @@ func TestGetVPNClientProfileEnsuresFabricVPNPacketRouteIntents(t *testing.T) {
|
||||
vpnClientProfile: VPNClientProfile{
|
||||
SchemaVersion: "rap.vpn_client_profile.v1",
|
||||
Connections: []VPNClientConnection{{
|
||||
ID: "vpn-1",
|
||||
ID: "vpn-1",
|
||||
TargetEndpoint: json.RawMessage(`{"type":"fabric_ipv4_exit_pool","exit_pool_ids":["home-ipv4"]}`),
|
||||
ClientConfig: json.RawMessage(`{
|
||||
"vpn_fabric_route": {
|
||||
"status": "planned",
|
||||
@@ -735,6 +865,34 @@ func TestGetVPNClientProfileEnsuresFabricVPNPacketRouteIntents(t *testing.T) {
|
||||
if session["preferred_transport"] != "fabric_service_channel_v1" || session["fallback_transport"] != "none" || session["backend_relay_allowed"] != false {
|
||||
t.Fatalf("unexpected dataplane session transports: %#v", session)
|
||||
}
|
||||
request, ok := session["fabric_service_channel_request"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("missing fabric service channel request in %#v", session)
|
||||
}
|
||||
if request["service_class"] != "vpn_packets" || request["source_role"] != "vpn-client" {
|
||||
t.Fatalf("unexpected fabric service channel request: %#v", request)
|
||||
}
|
||||
target := request["target"].(map[string]any)
|
||||
poolIDs := target["pool_ids"].([]any)
|
||||
if target["kind"] != "pool" || target["service_role"] != "ipv4-egress" || len(poolIDs) != 1 || poolIDs[0] != "home-ipv4" {
|
||||
t.Fatalf("unexpected fabric service channel target: %#v", target)
|
||||
}
|
||||
adapter := request["adapter_contract"].(map[string]any)
|
||||
if adapter["adapter_may_select_endpoint"] != false || adapter["adapter_may_use_legacy_relay"] != false {
|
||||
t.Fatalf("vpn adapter must not own transport decisions: %#v", adapter)
|
||||
}
|
||||
routeBundle, ok := session["fabric_route_bundle"].(map[string]any)
|
||||
if !ok || routeBundle["legacy_visibility"] != "opaque_to_service_adapters" {
|
||||
t.Fatalf("missing opaque route bundle: %#v", session["fabric_route_bundle"])
|
||||
}
|
||||
routeLease, ok := routeBundle["route_lease"].(map[string]any)
|
||||
if !ok || routeLease["schema_version"] != "rap.fabric_route_lease.v1" || routeLease["service_visibility"] != "opaque_route_lease" {
|
||||
t.Fatalf("missing route lease: %#v", routeBundle["route_lease"])
|
||||
}
|
||||
rebuildPolicy := routeLease["rebuild_policy"].(map[string]any)
|
||||
if rebuildPolicy["owner"] != "fabric_farm" || rebuildPolicy["service_adapter_action"] != "keep_sending_packets_to_channel" {
|
||||
t.Fatalf("unexpected route lease rebuild policy: %#v", rebuildPolicy)
|
||||
}
|
||||
if session["entry_node_id"] != "entry-1" || session["exit_node_id"] != "exit-1" {
|
||||
t.Fatalf("unexpected dataplane session route: %#v", session)
|
||||
}
|
||||
@@ -920,6 +1078,88 @@ func TestNodeUpdatePlanSelectsMatchingReleaseArtifact(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeUpdatePlanIncludesQuorumAuthorityWhenConfigured(t *testing.T) {
|
||||
keys, err := clusterauth.GenerateKeyPair()
|
||||
if err != nil {
|
||||
t.Fatalf("generate key: %v", err)
|
||||
}
|
||||
store := &fakeRepository{
|
||||
platformRole: PlatformRoleAdmin,
|
||||
clusterAuthority: ClusterAuthorityKey{
|
||||
ClusterAuthorityDescriptor: ClusterAuthorityDescriptor{
|
||||
SchemaVersion: clusterauth.AuthoritySchemaVersion,
|
||||
ClusterID: "cluster-1",
|
||||
AuthorityState: "active",
|
||||
KeyAlgorithm: clusterauth.AlgorithmEd25519,
|
||||
PublicKey: keys.PublicKeyB64,
|
||||
PublicKeyFingerprint: keys.Fingerprint,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
},
|
||||
PrivateKey: keys.PrivateKeyB64,
|
||||
QuorumDescriptor: &QuorumDescriptor{
|
||||
SchemaVersion: clusterauth.QuorumSchemaVersion,
|
||||
ClusterID: "cluster-1",
|
||||
Epoch: "epoch-1",
|
||||
Threshold: 1,
|
||||
Members: []clusterauth.QuorumMember{
|
||||
{
|
||||
NodeID: "authority-1",
|
||||
Role: "update-authority",
|
||||
PublicKey: keys.PublicKeyB64,
|
||||
PublicKeyFingerprint: keys.Fingerprint,
|
||||
Scopes: []string{"update-authority"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
releaseVersions: []ReleaseVersion{
|
||||
{
|
||||
ID: "release-1",
|
||||
ClusterID: "cluster-1",
|
||||
Product: "rap-node-agent",
|
||||
Version: "0.1.0-c17z26",
|
||||
Channel: "dev",
|
||||
Status: "active",
|
||||
Artifacts: []ReleaseArtifact{
|
||||
{ID: "docker", ClusterID: "cluster-1", Product: "rap-node-agent", Version: "0.1.0-c17z26", OS: "linux", Arch: "amd64", InstallType: "docker", Kind: "docker_image_tar", URL: "https://cache/agent.tar", SHA256: "docker-sha"},
|
||||
},
|
||||
},
|
||||
},
|
||||
nodeUpdatePolicies: map[string]NodeUpdatePolicy{
|
||||
"node-1|rap-node-agent": {
|
||||
ClusterID: "cluster-1",
|
||||
NodeID: "node-1",
|
||||
Product: "rap-node-agent",
|
||||
Channel: "dev",
|
||||
Strategy: "manual",
|
||||
Enabled: true,
|
||||
RollbackAllowed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
service := NewService(store)
|
||||
|
||||
plan, err := service.GetNodeUpdatePlan(context.Background(), GetNodeUpdatePlanInput{
|
||||
ClusterID: "cluster-1",
|
||||
NodeID: "node-1",
|
||||
Product: "rap-node-agent",
|
||||
CurrentVersion: "0.1.0-c17z25",
|
||||
OS: "linux",
|
||||
Arch: "amd64",
|
||||
InstallType: "docker",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update plan: %v", err)
|
||||
}
|
||||
if plan.AuthorityQuorum == nil {
|
||||
t.Fatalf("update plan must include quorum envelope: %+v", plan)
|
||||
}
|
||||
if err := clusterauth.VerifyQuorumRaw(*store.clusterAuthority.QuorumDescriptor, plan.AuthorityPayload, *plan.AuthorityQuorum, "update-authority"); err != nil {
|
||||
t.Fatalf("verify quorum authority: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeUpdatePlanAbsolutizesRelativeArtifactURLs(t *testing.T) {
|
||||
store := &fakeRepository{
|
||||
platformRole: PlatformRoleAdmin,
|
||||
@@ -1914,6 +2154,7 @@ func TestGetNodeSyntheticMeshConfigScopesPrivateBootstrapPeersForOutboundOnlyNod
|
||||
clusterNodes: []ClusterNode{
|
||||
{ID: "node-local", RegistrationStatus: NodeRegistrationActive, HealthStatus: "healthy", MembershipStatus: "active", CreatedAt: now.Add(-2 * time.Hour), LastSeenAt: ptrTime(now)},
|
||||
{ID: "node-peer", RegistrationStatus: NodeRegistrationActive, HealthStatus: "healthy", MembershipStatus: "active", CreatedAt: now.Add(-time.Hour), LastSeenAt: ptrTime(now.Add(-time.Second))},
|
||||
{ID: "node-relay", RegistrationStatus: NodeRegistrationActive, HealthStatus: "healthy", MembershipStatus: "active", CreatedAt: now.Add(-90 * time.Minute), LastSeenAt: ptrTime(now.Add(-2 * time.Second))},
|
||||
},
|
||||
nodeRoles: map[string][]NodeRoleAssignment{
|
||||
"node-local": {{NodeID: "node-local", Role: "core-mesh", Status: "active"}},
|
||||
@@ -1954,8 +2195,8 @@ func TestGetNodeSyntheticMeshConfigScopesPrivateBootstrapPeersForOutboundOnlyNod
|
||||
"endpoint_candidates": [{
|
||||
"endpoint_id": "node-peer-lan",
|
||||
"node_id": "node-peer",
|
||||
"transport": "direct_http",
|
||||
"address": "http://192.168.200.61:19133",
|
||||
"transport": "direct_quic",
|
||||
"address": "quic://192.168.200.61:19133",
|
||||
"reachability": "private",
|
||||
"connectivity_mode": "private_lan",
|
||||
"priority": 1
|
||||
@@ -1963,6 +2204,30 @@ func TestGetNodeSyntheticMeshConfigScopesPrivateBootstrapPeersForOutboundOnlyNod
|
||||
}
|
||||
}`),
|
||||
}},
|
||||
"node-relay": {{
|
||||
ClusterID: "cluster-1",
|
||||
NodeID: "node-relay",
|
||||
ObservedAt: now,
|
||||
Metadata: json.RawMessage(`{
|
||||
"mesh_endpoint_report": {
|
||||
"cluster_id": "cluster-1",
|
||||
"node_id": "node-relay",
|
||||
"peer_endpoint": "quic://relay.example.test:19131",
|
||||
"transport": "direct_quic",
|
||||
"connectivity_mode": "direct",
|
||||
"region": "public",
|
||||
"endpoint_candidates": [{
|
||||
"endpoint_id": "node-relay-public",
|
||||
"node_id": "node-relay",
|
||||
"transport": "direct_quic",
|
||||
"address": "quic://relay.example.test:19131",
|
||||
"reachability": "public",
|
||||
"connectivity_mode": "direct",
|
||||
"priority": 1
|
||||
}]
|
||||
}
|
||||
}`),
|
||||
}},
|
||||
},
|
||||
})
|
||||
service.now = func() time.Time { return now }
|
||||
@@ -1982,7 +2247,7 @@ func TestGetNodeSyntheticMeshConfigScopesPrivateBootstrapPeersForOutboundOnlyNod
|
||||
t.Fatalf("peer candidates = %+v, want relay-required candidate", cfg.PeerEndpointCandidates)
|
||||
}
|
||||
candidate := candidates[0]
|
||||
if candidate.Transport != "relay" || candidate.Reachability != "relay" || candidate.ConnectivityMode != "relay_required" {
|
||||
if candidate.Transport != "relay_quic" || candidate.Reachability != "relay" || candidate.ConnectivityMode != "relay_required" {
|
||||
t.Fatalf("candidate not converted to relay required: %+v", candidate)
|
||||
}
|
||||
if !containsString(candidate.PolicyTags, "offsite-private-lan-blocked") || !containsString(candidate.PolicyTags, "relay-required") {
|
||||
@@ -2002,10 +2267,10 @@ func TestGetNodeSyntheticMeshConfigScopesPrivateBootstrapPeersForOutboundOnlyNod
|
||||
}
|
||||
lease := cfg.RendezvousLeases[0]
|
||||
if lease.PeerNodeID != "node-peer" ||
|
||||
lease.RelayNodeID != "control-plane-relay" ||
|
||||
lease.RelayEndpoint != "https://control.example.test" ||
|
||||
lease.RelayNodeID != "node-relay" ||
|
||||
lease.RelayEndpoint != "quic://relay.example.test:19131" ||
|
||||
lease.Transport != "relay_control" ||
|
||||
lease.Reason != "control_plane_bootstrap_relay" ||
|
||||
lease.Reason != "farm_mesh_bootstrap_relay" ||
|
||||
!lease.ControlPlaneOnly {
|
||||
t.Fatalf("unexpected bootstrap rendezvous lease: %+v", lease)
|
||||
}
|
||||
@@ -2395,6 +2660,206 @@ func TestGetNodeSyntheticMeshConfigAppliesReplacementPathHintForExit(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoutePathDecisionUsesRendezvousLeaseForPassiveNATRoute(t *testing.T) {
|
||||
now := time.Date(2026, 5, 17, 3, 45, 0, 0, time.UTC)
|
||||
route := SyntheticMeshRouteConfig{
|
||||
RouteID: "route-a-b",
|
||||
ClusterID: "cluster-1",
|
||||
SourceNodeID: "node-a",
|
||||
DestinationNodeID: "node-b",
|
||||
Hops: []string{"node-a", "node-b"},
|
||||
AllowedChannels: []string{"fabric_control", "route_control"},
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
}
|
||||
decision := routePathDecisionForRoute(route, "node-a", []PeerRendezvousLease{{
|
||||
LeaseID: "route-a-b-rv-node-b-via-node-r",
|
||||
PeerNodeID: "node-b",
|
||||
RelayNodeID: "node-r",
|
||||
RelayEndpoint: "quic://node-r.example.test:19443",
|
||||
Transport: "relay_control",
|
||||
ConnectivityMode: "relay_required",
|
||||
RouteIDs: []string{"route-a-b"},
|
||||
Priority: 10,
|
||||
ControlPlaneOnly: true,
|
||||
IssuedAt: now,
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
Reason: "auto_rendezvous_required",
|
||||
}}, newRendezvousRelayPolicy("node-a", nil, now), "generation-1", fabricServiceChannelRouteFeedback{})
|
||||
|
||||
if decision.DecisionSource != "rendezvous_relay_required" ||
|
||||
decision.SelectedRelayID != "node-r" ||
|
||||
decision.SelectedRelayEndpoint != "quic://node-r.example.test:19443" ||
|
||||
decision.RendezvousPeerNodeID != "node-b" ||
|
||||
decision.RendezvousLeaseID != "route-a-b-rv-node-b-via-node-r" ||
|
||||
decision.RendezvousLeaseReason != "auto_rendezvous_required" ||
|
||||
decision.NextHopID != "node-r" ||
|
||||
decision.LocalRole != "entry" ||
|
||||
strings.Join(decision.EffectiveHops, ",") != "node-a,node-r,node-b" ||
|
||||
!decision.ControlPlaneOnly ||
|
||||
decision.ProductionForwarding {
|
||||
t.Fatalf("unexpected rendezvous route path decision: %+v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopedRendezvousLeasesKeepsOperatorPassiveNATLeaseWhenRelayFeedbackIsStale(t *testing.T) {
|
||||
now := time.Date(2026, 5, 17, 5, 15, 0, 0, time.UTC)
|
||||
route := SyntheticMeshRouteConfig{
|
||||
RouteID: "route-a-b",
|
||||
ClusterID: "cluster-1",
|
||||
SourceNodeID: "node-a",
|
||||
DestinationNodeID: "node-b",
|
||||
Hops: []string{"node-a", "node-b"},
|
||||
AllowedChannels: []string{"fabric_control", "route_control"},
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
}
|
||||
lease := PeerRendezvousLease{
|
||||
LeaseID: "route-a-b-rv-node-b-via-node-r",
|
||||
PeerNodeID: "node-b",
|
||||
RelayNodeID: "node-r",
|
||||
RelayEndpoint: "quic://node-r.example.test:19443",
|
||||
Transport: "relay_control",
|
||||
ConnectivityMode: "relay_required",
|
||||
RouteIDs: []string{"route-a-b"},
|
||||
Priority: 10,
|
||||
ControlPlaneOnly: true,
|
||||
IssuedAt: now,
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
Reason: "operator_rendezvous_required_for_passive_nat",
|
||||
}
|
||||
relayPolicy := newRendezvousRelayPolicy("node-a", nil, now)
|
||||
relayPolicy.addFeedback([]rendezvousRelayFeedbackEntry{{
|
||||
RouteIDs: []string{"route-a-b"},
|
||||
PeerNodeID: "node-b",
|
||||
RelayNodeID: "node-r",
|
||||
LeaseID: "route-a-b-rv-node-b-via-node-r",
|
||||
ReporterNodeID: "node-a",
|
||||
}})
|
||||
|
||||
leases := scopedRendezvousLeases([]PeerRendezvousLease{lease}, route, "node-a", relayPolicy, now)
|
||||
if len(leases) != 1 || leases[0].LeaseID != lease.LeaseID {
|
||||
t.Fatalf("operator passive NAT lease must remain scoped despite stale feedback: %+v", leases)
|
||||
}
|
||||
if report := relayPolicy.report(); report != nil && report.WithdrawnLeaseCount != 0 {
|
||||
t.Fatalf("operator passive NAT lease must not be withdrawn: %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDerivedRendezvousLeaseCanSelectRelayOutsideOriginalPath(t *testing.T) {
|
||||
now := time.Date(2026, 5, 17, 4, 30, 0, 0, time.UTC)
|
||||
route := SyntheticMeshRouteConfig{
|
||||
RouteID: "route-a-b",
|
||||
ClusterID: "cluster-1",
|
||||
SourceNodeID: "node-a",
|
||||
DestinationNodeID: "node-b",
|
||||
Hops: []string{"node-a", "node-b"},
|
||||
AllowedChannels: []string{"fabric_control", "route_control"},
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
}
|
||||
leases := derivedRendezvousLeases(route, map[string]string{}, map[string][]PeerEndpointCandidate{
|
||||
"node-b": {
|
||||
{
|
||||
EndpointID: "node-b-private",
|
||||
NodeID: "node-b",
|
||||
Transport: "direct_quic",
|
||||
Address: "quic://10.10.10.20:19131",
|
||||
Reachability: "private",
|
||||
ConnectivityMode: "private_lan",
|
||||
Region: "remote-lan",
|
||||
Priority: 5,
|
||||
LastVerifiedAt: &now,
|
||||
},
|
||||
},
|
||||
"node-r": {
|
||||
{
|
||||
EndpointID: "node-r-public",
|
||||
NodeID: "node-r",
|
||||
Transport: "direct_quic",
|
||||
Address: "quic://203.0.113.10:19131",
|
||||
Reachability: "public",
|
||||
ConnectivityMode: "direct",
|
||||
Region: "internet",
|
||||
Priority: 10,
|
||||
PolicyTags: []string{"fast-path"},
|
||||
LastVerifiedAt: &now,
|
||||
},
|
||||
},
|
||||
}, "node-a", endpointPerspective{Region: "home-lan"}, newRendezvousRelayPolicy("node-a", nil, now), now)
|
||||
|
||||
if len(leases) != 1 ||
|
||||
leases[0].PeerNodeID != "node-b" ||
|
||||
leases[0].RelayNodeID != "node-r" ||
|
||||
leases[0].RelayEndpoint != "quic://203.0.113.10:19131" ||
|
||||
leases[0].Reason != "auto_rendezvous_required" {
|
||||
t.Fatalf("unexpected derived rendezvous leases: %+v", leases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNodeSyntheticMeshConfigIncludesRendezvousRelayOutsideOriginalHops(t *testing.T) {
|
||||
now := time.Date(2026, 5, 17, 4, 15, 0, 0, time.UTC)
|
||||
service := NewService(&fakeRepository{
|
||||
testingFlags: EffectiveNodeTestingFlags{
|
||||
Enabled: true,
|
||||
SyntheticLinksEnabled: true,
|
||||
},
|
||||
routeIntents: []MeshRouteIntent{
|
||||
{
|
||||
ID: "route-a-b",
|
||||
ClusterID: "cluster-1",
|
||||
SourceSelector: json.RawMessage(`{"node_id":"node-a"}`),
|
||||
DestinationSelector: json.RawMessage(`{"node_id":"node-b"}`),
|
||||
ServiceClass: "vpn_packets",
|
||||
Status: "active",
|
||||
Policy: json.RawMessage(`{
|
||||
"synthetic_enabled": true,
|
||||
"hops": ["node-a", "node-b"],
|
||||
"allowed_channels": ["fabric_control", "route_control"],
|
||||
"expires_at": "2026-05-17T05:15:00Z",
|
||||
"rendezvous_leases": [
|
||||
{
|
||||
"lease_id": "route-a-b-rv-node-b-via-node-r",
|
||||
"peer_node_id": "node-b",
|
||||
"relay_node_id": "node-r",
|
||||
"relay_endpoint": "quic://node-r.example.test:19443",
|
||||
"transport": "relay_control",
|
||||
"connectivity_mode": "relay_required",
|
||||
"route_ids": ["route-a-b"],
|
||||
"allowed_channels": ["fabric_control", "route_control"],
|
||||
"priority": 10,
|
||||
"control_plane_only": true,
|
||||
"expires_at": "2026-05-17T05:15:00Z",
|
||||
"reason": "auto_rendezvous_required"
|
||||
}
|
||||
]
|
||||
}`),
|
||||
UpdatedAt: now,
|
||||
},
|
||||
},
|
||||
})
|
||||
service.now = func() time.Time { return now }
|
||||
|
||||
cfg, err := service.GetNodeSyntheticMeshConfig(context.Background(), GetNodeSyntheticMeshConfigInput{
|
||||
ClusterID: "cluster-1",
|
||||
NodeID: "node-r",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get synthetic config: %v", err)
|
||||
}
|
||||
if len(cfg.Routes) != 1 || strings.Join(cfg.Routes[0].Hops, ",") != "node-a,node-r,node-b" {
|
||||
t.Fatalf("relay scoped route missing effective hops: %+v", cfg.Routes)
|
||||
}
|
||||
if cfg.RoutePathDecisions == nil || len(cfg.RoutePathDecisions.Decisions) != 1 {
|
||||
t.Fatalf("relay route path decision missing: %+v", cfg.RoutePathDecisions)
|
||||
}
|
||||
decision := cfg.RoutePathDecisions.Decisions[0]
|
||||
if decision.SelectedRelayID != "node-r" ||
|
||||
decision.LocalRole != "selected_relay" ||
|
||||
decision.PreviousHopID != "node-a" ||
|
||||
decision.NextHopID != "node-b" ||
|
||||
strings.Join(decision.EffectiveHops, ",") != "node-a,node-r,node-b" {
|
||||
t.Fatalf("unexpected relay scoped decision: %+v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNodeSyntheticMeshConfigUsesRouteHealthDriftToReselectRelay(t *testing.T) {
|
||||
now := time.Date(2026, 4, 28, 12, 30, 0, 0, time.UTC)
|
||||
routeHealthMetadata, err := json.Marshal(map[string]any{
|
||||
|
||||
Reference in New Issue
Block a user