549 lines
16 KiB
TypeScript
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");
|
|
});
|