netbird-gitops/src/export.test.ts
2026-03-06 16:28:01 +02:00

549 lines
16 KiB
TypeScript

import { assertEquals } from "@std/assert";
import { exportState } from "./export.ts";
import type { ActualState } from "./state/actual.ts";
import type {
NbDnsNameserverGroup,
NbGroup,
NbNetwork,
NbNetworkResource,
NbNetworkRouter,
NbPeer,
NbPolicy,
NbPostureCheck,
NbRoute,
NbSetupKey,
NbUser,
} from "./netbird/types.ts";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Builds a minimal ActualState with indexed maps from raw arrays. */
function buildActualState(data: {
groups?: NbGroup[];
setupKeys?: NbSetupKey[];
peers?: NbPeer[];
policies?: NbPolicy[];
routes?: NbRoute[];
dns?: NbDnsNameserverGroup[];
postureChecks?: NbPostureCheck[];
networks?: NbNetwork[];
networkResources?: Map<string, NbNetworkResource[]>;
networkRouters?: Map<string, NbNetworkRouter[]>;
users?: NbUser[];
}): ActualState {
const groups = data.groups ?? [];
const setupKeys = data.setupKeys ?? [];
const peers = data.peers ?? [];
const policies = data.policies ?? [];
const routes = data.routes ?? [];
const dns = data.dns ?? [];
const postureChecks = data.postureChecks ?? [];
const networks = data.networks ?? [];
const users = data.users ?? [];
return {
groups,
groupsByName: new Map(groups.map((g) => [g.name, g])),
groupsById: new Map(groups.map((g) => [g.id, g])),
setupKeys,
setupKeysByName: new Map(setupKeys.map((k) => [k.name, k])),
peers,
peersByName: new Map(peers.map((p) => [p.name, p])),
peersById: new Map(peers.map((p) => [p.id, p])),
policies,
policiesByName: new Map(policies.map((p) => [p.name, p])),
routes,
routesByNetworkId: new Map(routes.map((r) => [r.network_id, r])),
dns,
dnsByName: new Map(dns.map((d) => [d.name, d])),
postureChecks,
postureChecksByName: new Map(postureChecks.map((pc) => [pc.name, pc])),
networks,
networksByName: new Map(networks.map((n) => [n.name, n])),
networkResources: data.networkResources ?? new Map(),
networkRouters: data.networkRouters ?? new Map(),
users,
usersByEmail: new Map(users.map((u) => [u.email, u])),
};
}
function makeGroup(
overrides: Partial<NbGroup> & Pick<NbGroup, "id" | "name">,
): NbGroup {
return {
peers_count: 0,
peers: [],
issued: "api",
...overrides,
};
}
function makeSetupKey(
overrides: Partial<NbSetupKey> & Pick<NbSetupKey, "name">,
): NbSetupKey {
return {
id: 1,
type: "one-off",
key: "NBSK-masked",
expires: "2027-01-01T00:00:00Z",
valid: true,
revoked: false,
used_times: 0,
state: "valid",
auto_groups: [],
usage_limit: 1,
...overrides,
};
}
// ---------------------------------------------------------------------------
// Tests: Normal state with groups, keys, policy
// ---------------------------------------------------------------------------
Deno.test("exportState: normal state with groups, keys, and policy", () => {
const actual = buildActualState({
groups: [
makeGroup({
id: "g-pilots",
name: "pilots",
peers: [{ id: "p1", name: "Pilot-hawk-72" }],
}),
makeGroup({ id: "g-vehicles", name: "vehicles" }),
],
setupKeys: [
makeSetupKey({
name: "Pilot-hawk-72",
auto_groups: ["g-pilots"],
used_times: 1,
usage_limit: 1,
}),
],
policies: [
{
id: "pol1",
name: "allow-pilot-vehicle",
description: "pilot to vehicle",
enabled: true,
source_posture_checks: [],
rules: [
{
name: "rule1",
description: "",
enabled: true,
action: "accept",
bidirectional: true,
protocol: "all",
sources: ["g-pilots"],
destinations: ["g-vehicles"],
},
],
},
],
});
const exported = exportState(actual);
// Groups exported with correct peer mapping
assertEquals(Object.keys(exported.groups), ["pilots", "vehicles"]);
assertEquals(exported.groups["pilots"].peers, ["Pilot-hawk-72"]);
assertEquals(exported.groups["vehicles"].peers, []);
// Setup key with auto_groups resolved to names
assertEquals(Object.keys(exported.setup_keys), ["Pilot-hawk-72"]);
assertEquals(exported.setup_keys["Pilot-hawk-72"].auto_groups, ["pilots"]);
assertEquals(exported.setup_keys["Pilot-hawk-72"].enrolled, true);
assertEquals(exported.setup_keys["Pilot-hawk-72"].type, "one-off");
assertEquals(exported.setup_keys["Pilot-hawk-72"].expires_in, 604800);
// Policy with source/destination resolved
assertEquals(Object.keys(exported.policies), ["allow-pilot-vehicle"]);
assertEquals(exported.policies["allow-pilot-vehicle"].sources, ["pilots"]);
assertEquals(exported.policies["allow-pilot-vehicle"].destinations, [
"vehicles",
]);
assertEquals(exported.policies["allow-pilot-vehicle"].bidirectional, true);
assertEquals(exported.policies["allow-pilot-vehicle"].protocol, "all");
assertEquals(exported.policies["allow-pilot-vehicle"].action, "accept");
});
// ---------------------------------------------------------------------------
// Tests: Empty state (only "All" group)
// ---------------------------------------------------------------------------
Deno.test("exportState: empty state with only All group produces empty export", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g-all", name: "All", issued: "jwt" }),
],
});
const exported = exportState(actual);
assertEquals(Object.keys(exported.groups).length, 0);
assertEquals(Object.keys(exported.setup_keys).length, 0);
assertEquals(Object.keys(exported.policies).length, 0);
assertEquals(Object.keys(exported.routes).length, 0);
assertEquals(Object.keys(exported.dns.nameserver_groups).length, 0);
});
// ---------------------------------------------------------------------------
// Tests: auto_groups ID-to-name mapping
// ---------------------------------------------------------------------------
Deno.test("exportState: auto_groups IDs are resolved to group names", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g-alpha", name: "alpha" }),
makeGroup({ id: "g-beta", name: "beta" }),
],
setupKeys: [
makeSetupKey({
name: "key-1",
auto_groups: ["g-alpha", "g-beta"],
}),
],
});
const exported = exportState(actual);
assertEquals(exported.setup_keys["key-1"].auto_groups, ["alpha", "beta"]);
});
Deno.test("exportState: auto_groups with unresolvable IDs are dropped", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g-alpha", name: "alpha" }),
],
setupKeys: [
makeSetupKey({
name: "key-1",
auto_groups: ["g-alpha", "g-nonexistent"],
}),
],
});
const exported = exportState(actual);
assertEquals(exported.setup_keys["key-1"].auto_groups, ["alpha"]);
});
// ---------------------------------------------------------------------------
// Tests: Enrolled detection
// ---------------------------------------------------------------------------
Deno.test("exportState: enrolled detection — used key is enrolled", () => {
const actual = buildActualState({
setupKeys: [
makeSetupKey({ name: "used-key", used_times: 1, usage_limit: 1 }),
],
});
assertEquals(exportState(actual).setup_keys["used-key"].enrolled, true);
});
Deno.test("exportState: enrolled detection — unused key is not enrolled", () => {
const actual = buildActualState({
setupKeys: [
makeSetupKey({ name: "fresh-key", used_times: 0, usage_limit: 1 }),
],
});
assertEquals(exportState(actual).setup_keys["fresh-key"].enrolled, false);
});
Deno.test("exportState: enrolled detection — unlimited reusable is never enrolled", () => {
const actual = buildActualState({
setupKeys: [
makeSetupKey({
name: "reusable-key",
type: "reusable",
used_times: 50,
usage_limit: 0,
}),
],
});
assertEquals(
exportState(actual).setup_keys["reusable-key"].enrolled,
false,
);
});
Deno.test("exportState: enrolled detection — partially used is not enrolled", () => {
const actual = buildActualState({
setupKeys: [
makeSetupKey({
name: "partial-key",
type: "reusable",
used_times: 2,
usage_limit: 5,
}),
],
});
assertEquals(
exportState(actual).setup_keys["partial-key"].enrolled,
false,
);
});
// ---------------------------------------------------------------------------
// Tests: System groups excluded
// ---------------------------------------------------------------------------
Deno.test("exportState: system groups are excluded", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g-all", name: "All", issued: "jwt" }),
makeGroup({ id: "g-jwt", name: "jwt-group", issued: "jwt" }),
makeGroup({
id: "g-int",
name: "integration-group",
issued: "integration",
}),
makeGroup({ id: "g-api", name: "user-group", issued: "api" }),
],
});
const exported = exportState(actual);
const groupNames = Object.keys(exported.groups);
assertEquals(groupNames, ["user-group"]);
});
Deno.test("exportState: All group with api issued is still excluded", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g-all", name: "All", issued: "api" }),
makeGroup({ id: "g-user", name: "my-group", issued: "api" }),
],
});
const exported = exportState(actual);
assertEquals(Object.keys(exported.groups), ["my-group"]);
});
// ---------------------------------------------------------------------------
// Tests: Group peers filter by setup key name
// ---------------------------------------------------------------------------
Deno.test("exportState: group peers only include names matching setup keys", () => {
const actual = buildActualState({
groups: [
makeGroup({
id: "g1",
name: "ops",
peers: [
{ id: "p1", name: "Pilot-hawk-72" },
{ id: "p2", name: "random-peer-no-key" },
],
}),
],
setupKeys: [
makeSetupKey({ name: "Pilot-hawk-72" }),
],
});
const exported = exportState(actual);
assertEquals(exported.groups["ops"].peers, ["Pilot-hawk-72"]);
});
// ---------------------------------------------------------------------------
// Tests: Policies
// ---------------------------------------------------------------------------
Deno.test("exportState: policies with empty rules are skipped", () => {
const actual = buildActualState({
policies: [
{
id: "pol1",
name: "empty-policy",
description: "no rules",
enabled: true,
source_posture_checks: [],
rules: [],
},
],
});
assertEquals(Object.keys(exportState(actual).policies).length, 0);
});
Deno.test("exportState: policy sources/destinations as {id,name} objects are resolved", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g-src", name: "source-group" }),
makeGroup({ id: "g-dst", name: "dest-group" }),
],
policies: [
{
id: "pol1",
name: "object-refs",
description: "",
enabled: true,
source_posture_checks: [],
rules: [
{
name: "r1",
description: "",
enabled: true,
action: "accept",
bidirectional: false,
protocol: "tcp",
ports: ["443", "8080"],
sources: [{ id: "g-src", name: "source-group" }],
destinations: [{ id: "g-dst", name: "dest-group" }],
},
],
},
],
});
const exported = exportState(actual);
assertEquals(exported.policies["object-refs"].sources, ["source-group"]);
assertEquals(exported.policies["object-refs"].destinations, ["dest-group"]);
assertEquals(exported.policies["object-refs"].protocol, "tcp");
assertEquals(exported.policies["object-refs"].ports, ["443", "8080"]);
assertEquals(exported.policies["object-refs"].bidirectional, false);
});
Deno.test("exportState: policy without ports omits the ports field", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g1", name: "g" }),
],
policies: [
{
id: "pol1",
name: "no-ports",
description: "",
enabled: true,
source_posture_checks: [],
rules: [
{
name: "r1",
description: "",
enabled: true,
action: "accept",
bidirectional: true,
protocol: "all",
sources: ["g1"],
destinations: ["g1"],
},
],
},
],
});
const exported = exportState(actual);
assertEquals(exported.policies["no-ports"].ports, undefined);
});
// ---------------------------------------------------------------------------
// Tests: Routes
// ---------------------------------------------------------------------------
Deno.test("exportState: routes keyed by network_id with IDs resolved", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g-pg", name: "peer-group" }),
makeGroup({ id: "g-dist", name: "dist-group" }),
],
routes: [
{
id: "r1",
description: "LAN route",
network_id: "lan-net",
enabled: true,
peer_groups: ["g-pg"],
network: "10.0.0.0/24",
metric: 100,
masquerade: true,
groups: ["g-dist"],
keep_route: false,
},
],
});
const exported = exportState(actual);
assertEquals(Object.keys(exported.routes), ["lan-net"]);
assertEquals(exported.routes["lan-net"].peer_groups, ["peer-group"]);
assertEquals(exported.routes["lan-net"].distribution_groups, ["dist-group"]);
assertEquals(exported.routes["lan-net"].network, "10.0.0.0/24");
assertEquals(exported.routes["lan-net"].metric, 100);
assertEquals(exported.routes["lan-net"].masquerade, true);
assertEquals(exported.routes["lan-net"].enabled, true);
assertEquals(exported.routes["lan-net"].keep_route, false);
});
Deno.test("exportState: route with domains and no network", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g1", name: "grp" }),
],
routes: [
{
id: "r2",
description: "DNS route",
network_id: "dns-route",
enabled: true,
peer_groups: ["g1"],
domains: ["example.com"],
metric: 9999,
masquerade: false,
groups: ["g1"],
keep_route: true,
},
],
});
const exported = exportState(actual);
assertEquals(exported.routes["dns-route"].domains, ["example.com"]);
assertEquals(exported.routes["dns-route"].network, undefined);
});
// ---------------------------------------------------------------------------
// Tests: DNS
// ---------------------------------------------------------------------------
Deno.test("exportState: DNS nameserver groups with IDs resolved", () => {
const actual = buildActualState({
groups: [
makeGroup({ id: "g-dns", name: "dns-group" }),
],
dns: [
{
id: "d1",
name: "internal-dns",
description: "internal resolver",
nameservers: [{ ip: "1.1.1.1", ns_type: "udp", port: 53 }],
enabled: true,
groups: ["g-dns"],
primary: true,
domains: ["internal."],
search_domains_enabled: false,
},
],
});
const exported = exportState(actual);
assertEquals(Object.keys(exported.dns.nameserver_groups), ["internal-dns"]);
const ns = exported.dns.nameserver_groups["internal-dns"];
assertEquals(ns.groups, ["dns-group"]);
assertEquals(ns.nameservers, [{ ip: "1.1.1.1", ns_type: "udp", port: 53 }]);
assertEquals(ns.primary, true);
assertEquals(ns.domains, ["internal."]);
assertEquals(ns.search_domains_enabled, false);
assertEquals(ns.enabled, true);
assertEquals(ns.description, "internal resolver");
});