53b96da272
Replace the node-level DNS-name model with relative DNS hostnames and derived domain state. Derive apex and node-alias DNS hosts, public hosts, certificate subjects, challenge config, and certificate reconfigure operations while keeping IP-only deployments first-class.
1358 lines
38 KiB
TypeScript
1358 lines
38 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import test from "node:test"
|
|
|
|
import {
|
|
APP_STATE_VERSION,
|
|
type AppSnapshot,
|
|
type DnsRecordSet,
|
|
type DomainPlan,
|
|
type ProductCatalog,
|
|
type ProviderConfig,
|
|
type SchemeState,
|
|
type ServerPlan,
|
|
type StoredState,
|
|
type VpsRecord
|
|
} from "../../src/shared/app"
|
|
import {
|
|
buildPlannedDomainRemoval,
|
|
buildPlannedDomainUpsert,
|
|
buildPlannedNodeRemoval,
|
|
buildPlannedNodeUpsert,
|
|
findPlannedNodeOffers,
|
|
getPlannedDomainNameServers
|
|
} from "../../src/main/planned-resources"
|
|
import { NodeCliService } from "../../src/main/cli/node-cli-service"
|
|
import { deriveDomainDnsHosts } from "../../src/main/domain-hosts"
|
|
import { resolveServer } from "../../src/main/node-admin-client"
|
|
import type { LegionEngine } from "../../src/engine/runtime"
|
|
|
|
function createCatalog(providerKind: "hetzner" | "ovh" | "scaleway"): ProductCatalog {
|
|
return {
|
|
providerKind,
|
|
disclaimer: "",
|
|
isDefaultProvider: false,
|
|
generatedAt: "2026-04-07T00:00:00.000Z",
|
|
defaultProductId: `${providerKind}-dev1-m`,
|
|
products: [
|
|
{
|
|
id: `${providerKind}-dev1-m`,
|
|
providerKind,
|
|
providerProductId: "dev1-m",
|
|
label: "DEV1-M",
|
|
planName: "DEV1-M",
|
|
description: "",
|
|
architecture: "x86_64",
|
|
category: "general-purpose",
|
|
cores: 3,
|
|
cpuType: "shared",
|
|
memoryGb: 4,
|
|
diskGb: 40,
|
|
storageType: "ssd",
|
|
image: "debian-12",
|
|
bootMode: providerKind === "scaleway" ? "efi" : "bios",
|
|
defaultRegion: "fr-par-1",
|
|
availableRegions: ["fr-par-1"],
|
|
defaultPrice: {
|
|
region: "fr-par-1",
|
|
hourlyGrossEur: 0.03,
|
|
hourlyNetEur: 0.025,
|
|
monthlyGrossEur: 18,
|
|
monthlyNetEur: 15
|
|
},
|
|
prices: [
|
|
{
|
|
region: "fr-par-1",
|
|
hourlyGrossEur: 0.03,
|
|
hourlyNetEur: 0.025,
|
|
monthlyGrossEur: 18,
|
|
monthlyNetEur: 15
|
|
}
|
|
],
|
|
variables: [
|
|
{
|
|
key: "diskGb",
|
|
label: "Disk",
|
|
type: "integer",
|
|
unit: "GB",
|
|
defaultValue: 40,
|
|
min: 40,
|
|
max: 200,
|
|
step: 10,
|
|
affects: "diskGb"
|
|
}
|
|
],
|
|
commercialMetadata: {
|
|
effectiveMonthlyGrossEur: 18,
|
|
source: "provider",
|
|
notes: []
|
|
},
|
|
recommended: true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
function createProviderConfig(): ProviderConfig {
|
|
return {
|
|
id: "scaleway-e2e",
|
|
kind: "scaleway",
|
|
configured: true,
|
|
createdAt: "2026-04-07T00:00:00.000Z",
|
|
projectId: "project-1",
|
|
capabilities: {
|
|
computeCatalog: true,
|
|
computeProvisioning: true,
|
|
computeReinstallFromImage: false,
|
|
domainRegistration: false,
|
|
standardDnsService: false,
|
|
fallbackDnsService: false,
|
|
dnsRecordManagement: false,
|
|
firewallManagement: false
|
|
},
|
|
credentials: {
|
|
kind: "scaleway",
|
|
accessKey: "access",
|
|
secretKey: "secret",
|
|
projectId: "project-1"
|
|
}
|
|
}
|
|
}
|
|
|
|
function createOvhProviderConfig(): ProviderConfig {
|
|
return {
|
|
id: "ovh",
|
|
kind: "ovh",
|
|
configured: true,
|
|
createdAt: "2026-04-07T00:00:00.000Z",
|
|
capabilities: {
|
|
computeCatalog: true,
|
|
computeProvisioning: true,
|
|
computeReinstallFromImage: false,
|
|
domainRegistration: true,
|
|
standardDnsService: true,
|
|
fallbackDnsService: false,
|
|
dnsRecordManagement: true,
|
|
firewallManagement: false
|
|
},
|
|
credentials: {
|
|
kind: "ovh",
|
|
endpoint: "ovh-eu",
|
|
applicationKey: "app-key",
|
|
applicationSecret: "app-secret",
|
|
consumerKey: "consumer-key"
|
|
}
|
|
}
|
|
}
|
|
|
|
function createStoredState(overrides: Partial<StoredState> = {}): StoredState {
|
|
return {
|
|
version: APP_STATE_VERSION,
|
|
tribe: { name: "Tribe", description: "Test tribe", visibility: "invite_only" },
|
|
legionAdmin: null,
|
|
clusterBootstrap: null,
|
|
providers: [createProviderConfig()],
|
|
bindings: [],
|
|
trackedServers: [],
|
|
scheme: {
|
|
servers: [],
|
|
domains: [],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
},
|
|
actual: {
|
|
servers: [],
|
|
domains: [],
|
|
dnsZones: []
|
|
},
|
|
tutorial: { completed: true },
|
|
ui: overrides.ui ?? {},
|
|
...overrides,
|
|
pendingProvisioning: overrides.pendingProvisioning ?? [],
|
|
clusterMembership: overrides.clusterMembership ?? []
|
|
}
|
|
}
|
|
|
|
function createTrackedServer(overrides: Partial<VpsRecord> = {}): VpsRecord {
|
|
return {
|
|
id: "node-a",
|
|
providerId: "scaleway-e2e",
|
|
providerKind: "scaleway",
|
|
providerProjectId: "project-1",
|
|
providerServerId: "srv-123",
|
|
providerSshKeyId: "key-1",
|
|
providerSshKeyName: "key-1",
|
|
providerResourceName: "legion-node-a",
|
|
role: "primary",
|
|
sshUsername: "root",
|
|
sshPrivateKey: "PRIVATE",
|
|
sshPublicKey: "PUBLIC",
|
|
bootstrapSshAuthentication: null,
|
|
status: "running",
|
|
publicIp: "203.0.113.10",
|
|
region: "fr-par-1",
|
|
planName: "DEV1-M",
|
|
billingTermMonths: 1,
|
|
commercialMetadata: {
|
|
effectiveMonthlyGrossEur: 18,
|
|
source: "provider",
|
|
notes: []
|
|
},
|
|
deployment: {
|
|
status: "ready",
|
|
snippets: [],
|
|
lastAppliedAt: "2026-04-07T00:00:00.000Z"
|
|
},
|
|
createdAt: "2026-04-07T00:00:00.000Z",
|
|
updatedAt: "2026-04-07T00:00:00.000Z",
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function createServerPlan(overrides: Partial<ServerPlan> = {}): ServerPlan {
|
|
return {
|
|
id: "node-a",
|
|
label: "node-a",
|
|
providerKind: "scaleway",
|
|
providerId: "scaleway-e2e",
|
|
source: "catalog",
|
|
instanceId: "scaleway-dev1-m",
|
|
planName: "DEV1-M",
|
|
region: "fr-par-1",
|
|
commercialMetadata: {
|
|
effectiveMonthlyGrossEur: 18,
|
|
source: "provider",
|
|
notes: []
|
|
},
|
|
cores: 3,
|
|
memoryGb: 4,
|
|
diskGb: 80,
|
|
role: "primary",
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function createDomainPlan(overrides: Partial<DomainPlan> = {}): DomainPlan {
|
|
return {
|
|
id: "domain-1",
|
|
domain: "example.test",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
source: "managed",
|
|
delegatedZoneId: "dns-domain-1",
|
|
label: "example.test",
|
|
autoRenew: true,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
test("resolveServer accepts unique tracked node ID prefixes", () => {
|
|
const state = createStoredState({
|
|
trackedServers: [
|
|
createTrackedServer({ id: "79d84fde-bca0-4c1c-9c7e-2827e285d8e8" }),
|
|
createTrackedServer({ id: "01f62f94-1a0d-43f5-a945-c7729f340f3e" })
|
|
]
|
|
})
|
|
|
|
const server = resolveServer(state, "79d84fde")
|
|
|
|
assert.equal(server.id, "79d84fde-bca0-4c1c-9c7e-2827e285d8e8")
|
|
})
|
|
|
|
test("resolveServer accepts unique provider server ID prefixes", () => {
|
|
const state = createStoredState({
|
|
trackedServers: [
|
|
createTrackedServer({ id: "node-a", providerServerId: "srv-79d84fde" }),
|
|
createTrackedServer({ id: "node-b", providerServerId: "srv-01f62f94" })
|
|
]
|
|
})
|
|
|
|
const server = resolveServer(state, "srv-79")
|
|
|
|
assert.equal(server.id, "node-a")
|
|
})
|
|
|
|
test("resolveServer rejects ambiguous tracked node ID prefixes", () => {
|
|
const state = createStoredState({
|
|
trackedServers: [
|
|
createTrackedServer({ id: "79d84fde-bca0-4c1c-9c7e-2827e285d8e8" }),
|
|
createTrackedServer({ id: "79d84abc-1a0d-43f5-a945-c7729f340f3e" })
|
|
]
|
|
})
|
|
|
|
assert.throws(
|
|
() => resolveServer(state, "79d84"),
|
|
/Ambiguous node reference: 79d84 matches 79d84fde-bca0-4c1c-9c7e-2827e285d8e8, 79d84abc-1a0d-43f5-a945-c7729f340f3e/
|
|
)
|
|
})
|
|
|
|
test("buildPlannedNodeRemoval accepts unique planned node ID prefixes", () => {
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [
|
|
createServerPlan({ id: "79d84fde-bca0-4c1c-9c7e-2827e285d8e8" }),
|
|
createServerPlan({ id: "01f62f94-1a0d-43f5-a945-c7729f340f3e" })
|
|
],
|
|
domains: [],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
}
|
|
})
|
|
|
|
const { result } = buildPlannedNodeRemoval(state, "79d84fde")
|
|
|
|
assert.equal(result.serverId, "79d84fde-bca0-4c1c-9c7e-2827e285d8e8")
|
|
})
|
|
|
|
test("buildPlannedNodeRemoval rejects ambiguous planned node ID prefixes", () => {
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [
|
|
createServerPlan({ id: "79d84fde-bca0-4c1c-9c7e-2827e285d8e8" }),
|
|
createServerPlan({ id: "79d84abc-1a0d-43f5-a945-c7729f340f3e" })
|
|
],
|
|
domains: [],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
}
|
|
})
|
|
|
|
assert.throws(
|
|
() => buildPlannedNodeRemoval(state, "79d84"),
|
|
/Ambiguous node reference: 79d84 matches 79d84fde-bca0-4c1c-9c7e-2827e285d8e8, 79d84abc-1a0d-43f5-a945-c7729f340f3e/
|
|
)
|
|
})
|
|
|
|
test("buildPlannedNodeRemoval rejects ambiguous prefixes across planned and tracked nodes", () => {
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [createServerPlan({ id: "79d84fde-bca0-4c1c-9c7e-2827e285d8e8" })],
|
|
domains: [],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
},
|
|
trackedServers: [createTrackedServer({ id: "79d84abc-1a0d-43f5-a945-c7729f340f3e" })]
|
|
})
|
|
|
|
assert.throws(
|
|
() => buildPlannedNodeRemoval(state, "79d84"),
|
|
/Ambiguous node reference: 79d84 matches 79d84fde-bca0-4c1c-9c7e-2827e285d8e8, 79d84abc-1a0d-43f5-a945-c7729f340f3e/
|
|
)
|
|
})
|
|
|
|
test("buildPlannedDomainRemoval accepts unique domain ID prefixes", () => {
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [],
|
|
domains: [
|
|
createDomainPlan({ id: "domain-79d84fde-bca0" }),
|
|
createDomainPlan({ id: "domain-01f62f94-1a0d", domain: "other.test", label: "other.test" })
|
|
],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
}
|
|
})
|
|
|
|
const { domain } = buildPlannedDomainRemoval(state, "domain-79")
|
|
|
|
assert.equal(domain.id, "domain-79d84fde-bca0")
|
|
})
|
|
|
|
test("buildPlannedDomainRemoval rejects ambiguous domain ID prefixes", () => {
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [],
|
|
domains: [
|
|
createDomainPlan({ id: "domain-79d84fde-bca0" }),
|
|
createDomainPlan({ id: "domain-79d84abc-1a0d", domain: "other.test", label: "other.test" })
|
|
],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
}
|
|
})
|
|
|
|
assert.throws(
|
|
() => buildPlannedDomainRemoval(state, "domain-79d84"),
|
|
/Ambiguous domain reference: domain-79d84 matches domain-79d84fde-bca0, domain-79d84abc-1a0d/
|
|
)
|
|
})
|
|
|
|
test("listNodes prefers planned labels over provider resource names", () => {
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [
|
|
{
|
|
id: "node-a",
|
|
label: "Primary server",
|
|
providerKind: "scaleway",
|
|
providerId: "scaleway-e2e",
|
|
source: "catalog",
|
|
instanceId: "scaleway-dev1-m",
|
|
planName: "DEV1-M",
|
|
region: "fr-par-1",
|
|
commercialMetadata: {
|
|
effectiveMonthlyGrossEur: 18,
|
|
source: "provider",
|
|
notes: []
|
|
},
|
|
cores: 3,
|
|
memoryGb: 4,
|
|
diskGb: 80,
|
|
role: "primary"
|
|
}
|
|
],
|
|
domains: [],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
},
|
|
trackedServers: [
|
|
createTrackedServer({
|
|
providerResourceName: "steffen-79d84fde-bca0-4c1c-9c7e-2827e285"
|
|
})
|
|
]
|
|
})
|
|
const { runtime } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const [entry] = service.listNodes()
|
|
|
|
assert.equal(entry?.name, "steffen-79d84fde-bca0-4c1c-9c7e-2827e285")
|
|
assert.equal(entry?.label, "Primary server")
|
|
})
|
|
|
|
test("listNodes omits labels that duplicate provider resource names", () => {
|
|
const providerResourceName = "steffen-79d84fde-bca0-4c1c-9c7e-2827e285"
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [
|
|
{
|
|
id: "node-a",
|
|
label: providerResourceName,
|
|
providerKind: "scaleway",
|
|
providerId: "scaleway-e2e",
|
|
source: "catalog",
|
|
instanceId: "scaleway-dev1-m",
|
|
planName: "DEV1-M",
|
|
region: "fr-par-1",
|
|
commercialMetadata: {
|
|
effectiveMonthlyGrossEur: 18,
|
|
source: "provider",
|
|
notes: []
|
|
},
|
|
cores: 3,
|
|
memoryGb: 4,
|
|
diskGb: 80,
|
|
role: "primary"
|
|
}
|
|
],
|
|
domains: [],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
},
|
|
trackedServers: [
|
|
createTrackedServer({
|
|
providerResourceName
|
|
})
|
|
]
|
|
})
|
|
const { runtime } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const [entry] = service.listNodes()
|
|
|
|
assert.equal(entry?.name, providerResourceName)
|
|
assert.equal(entry?.label, undefined)
|
|
})
|
|
|
|
test("getNodeInfo returns detailed node fields with planned labels", () => {
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [createServerPlan({ id: "node-a", label: "Primary server" })],
|
|
domains: [],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
},
|
|
trackedServers: [
|
|
createTrackedServer({
|
|
id: "node-a",
|
|
providerServerId: "srv-79d84fde",
|
|
providerResourceName: "steffen-node-a",
|
|
nbde: {
|
|
mode: "degraded",
|
|
tangPort: 7654,
|
|
recoverySecret: "secret",
|
|
localBootKeyPresent: true,
|
|
peerTangNodeIds: []
|
|
},
|
|
execution: {
|
|
operationId: "operation-1",
|
|
operation: "add",
|
|
status: "failed",
|
|
phase: "rebooting-installed-system",
|
|
retryable: true,
|
|
startedAt: "2026-06-25T11:00:00.000Z",
|
|
lastError: "ssh timeout",
|
|
updatedAt: "2026-06-25T12:00:00.000Z"
|
|
}
|
|
})
|
|
]
|
|
})
|
|
const { runtime } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const entry = service.getNodeInfo("srv-79")
|
|
|
|
assert.equal(entry.id, "node-a")
|
|
assert.equal(entry.name, "steffen-node-a")
|
|
assert.equal(entry.label, "Primary server")
|
|
assert.equal(entry.providerServerId, "srv-79d84fde")
|
|
assert.equal(entry.nbdeMode, "degraded")
|
|
assert.equal(entry.executionStatus, "failed")
|
|
assert.equal(entry.executionPhase, "rebooting-installed-system")
|
|
assert.equal(entry.executionRetryable, true)
|
|
assert.equal(entry.executionError, "ssh timeout")
|
|
})
|
|
|
|
function createRuntime(state: StoredState): {
|
|
runtime: LegionEngine
|
|
materializeCalls: { count: number }
|
|
updateSchemeCalls: { count: number; last?: SchemeState }
|
|
} {
|
|
const materializeCalls = { count: 0 }
|
|
const updateSchemeCalls: { count: number; last?: SchemeState } = { count: 0 }
|
|
|
|
const createSnapshot = (): AppSnapshot => ({
|
|
tribe: state.tribe,
|
|
legionAdmin: state.legionAdmin
|
|
? {
|
|
id: state.legionAdmin.id,
|
|
username: state.legionAdmin.username,
|
|
role: state.legionAdmin.role,
|
|
publicKeyHex: state.legionAdmin.publicKeyHex,
|
|
npub: state.legionAdmin.npub,
|
|
createdAt: state.legionAdmin.createdAt,
|
|
updatedAt: state.legionAdmin.updatedAt
|
|
}
|
|
: null,
|
|
providers: state.providers,
|
|
scheme: state.scheme,
|
|
instances: {
|
|
servers: [],
|
|
domains: [],
|
|
dnsHosts: state.scheme.dnsHosts,
|
|
dnsZones: []
|
|
},
|
|
plannedChanges: [],
|
|
isConfigured: true,
|
|
tutorial: state.tutorial,
|
|
ui: state.ui,
|
|
hasPendingChanges: false
|
|
})
|
|
|
|
const plannedResources = {
|
|
getState: () => state,
|
|
getProductCatalog: async () => createCatalog("scaleway"),
|
|
getDnsRecordSets: async () =>
|
|
[
|
|
{
|
|
id: "ns-root",
|
|
providerKind: "ovh",
|
|
zoneName: "example.test",
|
|
name: "@",
|
|
type: "NS",
|
|
values: [{ value: "ns10.ovh.net." }, { value: "dns10.ovh.net." }],
|
|
observedAt: "2026-04-07T00:00:00.000Z"
|
|
}
|
|
] satisfies DnsRecordSet[],
|
|
quoteOvhDomainRegistration: async () => ({
|
|
domainName: "example.test",
|
|
available: true,
|
|
offers: [
|
|
{
|
|
duration: "P1Y",
|
|
months: 12,
|
|
currency: "EUR",
|
|
totalGrossEur: 12,
|
|
monthlyGrossEur: 1,
|
|
request: {
|
|
planCode: "com",
|
|
pricingMode: "create-default"
|
|
}
|
|
}
|
|
]
|
|
})
|
|
}
|
|
|
|
const runtime = {
|
|
stateStore: {
|
|
hasUnlockedState: () => true,
|
|
getStateOrThrow: () => state,
|
|
rekey: async () => undefined,
|
|
exportStateFile: async () => undefined,
|
|
importStateFile: async () => undefined,
|
|
upsertActualDomain: async () => undefined,
|
|
removeActualDomain: async () => undefined,
|
|
replaceActualDomains: async () => undefined
|
|
},
|
|
getSnapshot: createSnapshot,
|
|
provisioning: {
|
|
getActivity: () => null
|
|
},
|
|
nodeDeployments: {
|
|
setReporter: () => undefined,
|
|
reinstallNode: async () => createTrackedServer(),
|
|
retryNode: async () => createTrackedServer(),
|
|
promoteNbdeNode: async () => createTrackedServer(),
|
|
reconcileNbde: async () => []
|
|
},
|
|
getProductCatalog: plannedResources.getProductCatalog,
|
|
getDnsRecordSets: plannedResources.getDnsRecordSets,
|
|
findNodeOffers: async (request) => findPlannedNodeOffers(plannedResources, request),
|
|
upsertServerPlan: async (request) => {
|
|
const { scheme, result } = await buildPlannedNodeUpsert(plannedResources, request)
|
|
state.scheme = scheme
|
|
updateSchemeCalls.count += 1
|
|
updateSchemeCalls.last = scheme
|
|
return result
|
|
},
|
|
removeServerPlan: async (request: { serverId: string }) => {
|
|
const { scheme, result } = buildPlannedNodeRemoval(state, request.serverId)
|
|
state.scheme = scheme
|
|
updateSchemeCalls.count += 1
|
|
updateSchemeCalls.last = scheme
|
|
return result
|
|
},
|
|
upsertDomainPlan: async (request) => {
|
|
const { scheme, result } = buildPlannedDomainUpsert(plannedResources, request)
|
|
state.scheme = scheme
|
|
updateSchemeCalls.count += 1
|
|
updateSchemeCalls.last = scheme
|
|
return result
|
|
},
|
|
removeDomainPlan: async (ref: string) => {
|
|
const { scheme, domain } = buildPlannedDomainRemoval(state, ref)
|
|
state.scheme = scheme
|
|
updateSchemeCalls.count += 1
|
|
updateSchemeCalls.last = scheme
|
|
return domain
|
|
},
|
|
getPlannedDomainNameServers: async (ref: string) =>
|
|
getPlannedDomainNameServers(plannedResources, ref),
|
|
quoteDomain: async () => ({
|
|
domain: "example.test",
|
|
provider: "ovh",
|
|
providerId: "ovh",
|
|
available: true,
|
|
source: "ovh-order-api",
|
|
offers: [
|
|
{
|
|
months: 12,
|
|
duration: "P1Y",
|
|
currency: "EUR",
|
|
totalGrossEur: 12,
|
|
monthlyGrossEur: 1,
|
|
planCode: "com",
|
|
pricingMode: "create-default",
|
|
offerId: undefined
|
|
}
|
|
]
|
|
}),
|
|
unlock: async () => ({
|
|
snapshot: state,
|
|
activity: null,
|
|
auth: {
|
|
locked: false,
|
|
availableMethods: ["password"]
|
|
}
|
|
}),
|
|
updateScheme: async ({ scheme }: { scheme: SchemeState }) => {
|
|
state.scheme = scheme
|
|
updateSchemeCalls.count += 1
|
|
updateSchemeCalls.last = scheme
|
|
return {
|
|
snapshot: state,
|
|
activity: null,
|
|
auth: {
|
|
locked: false,
|
|
availableMethods: ["password"]
|
|
}
|
|
}
|
|
},
|
|
materialize: async () => {
|
|
materializeCalls.count += 1
|
|
state.trackedServers = state.scheme.servers.map((server) =>
|
|
createTrackedServer({
|
|
id: server.id,
|
|
providerKind: server.providerKind === "manual" ? "scaleway" : server.providerKind,
|
|
providerId: server.providerId ?? "scaleway-e2e",
|
|
providerResourceName: `legion-${server.id}`,
|
|
publicIp: server.publicIp ?? "203.0.113.10",
|
|
region: server.region,
|
|
planName: server.planName
|
|
})
|
|
)
|
|
return {
|
|
snapshot: state,
|
|
activity: null,
|
|
auth: {
|
|
locked: false,
|
|
availableMethods: ["password"]
|
|
}
|
|
}
|
|
}
|
|
} as unknown as LegionEngine
|
|
|
|
return {
|
|
runtime,
|
|
materializeCalls,
|
|
updateSchemeCalls
|
|
}
|
|
}
|
|
|
|
test("rekeyConfig unlocks with the current password and persists the new unlock password", async () => {
|
|
const state = createStoredState()
|
|
let unlockedWith: string | null = null
|
|
let rekeyedWith: { current?: string; next: string } | null = null
|
|
|
|
const runtime = {
|
|
stateStore: {
|
|
hasUnlockedState: () => false,
|
|
getStateOrThrow: () => state,
|
|
getSnapshot: () =>
|
|
({
|
|
tribe: state.tribe,
|
|
legionAdmin: null,
|
|
providers: state.providers,
|
|
scheme: state.scheme,
|
|
instances: {
|
|
servers: [],
|
|
domains: [],
|
|
dnsHosts: state.scheme.dnsHosts,
|
|
dnsZones: []
|
|
},
|
|
plannedChanges: [],
|
|
isConfigured: true,
|
|
tutorial: state.tutorial,
|
|
ui: state.ui,
|
|
hasPendingChanges: false
|
|
}) satisfies AppSnapshot,
|
|
rekey: async (currentPassword: string | undefined, nextPassword: string) => {
|
|
rekeyedWith = {
|
|
current: currentPassword,
|
|
next: nextPassword
|
|
}
|
|
},
|
|
exportStateFile: async () => undefined,
|
|
importStateFile: async () => undefined,
|
|
upsertActualDomain: async () => undefined,
|
|
removeActualDomain: async () => undefined,
|
|
replaceActualDomains: async () => undefined
|
|
},
|
|
provisioning: {
|
|
getActivity: () => null
|
|
},
|
|
nodeDeployments: {
|
|
setReporter: () => undefined,
|
|
reinstallNode: async () => createTrackedServer(),
|
|
retryNode: async () => createTrackedServer(),
|
|
promoteNbdeNode: async () => createTrackedServer(),
|
|
reconcileNbde: async () => []
|
|
},
|
|
unlock: async ({ method }: { method?: { type: "password"; password: string } }) => {
|
|
unlockedWith = method?.password ?? null
|
|
return {
|
|
snapshot: state,
|
|
activity: null,
|
|
auth: {
|
|
locked: false,
|
|
availableMethods: ["password"]
|
|
}
|
|
}
|
|
}
|
|
} as unknown as LegionEngine
|
|
|
|
process.env["LEGION_NEXT_UNLOCK_PASSWORD"] = "new-password"
|
|
|
|
try {
|
|
const service = new NodeCliService(runtime)
|
|
const result = await service.rekeyConfig({
|
|
currentPassword: "old-password",
|
|
newPasswordEnv: "LEGION_NEXT_UNLOCK_PASSWORD"
|
|
})
|
|
|
|
assert.equal(unlockedWith, "old-password")
|
|
assert.deepEqual(rekeyedWith, {
|
|
current: "old-password",
|
|
next: "new-password"
|
|
})
|
|
assert.equal(result.config.name, "Tribe")
|
|
assert.equal(result.config.description, "Test tribe")
|
|
} finally {
|
|
delete process.env["LEGION_NEXT_UNLOCK_PASSWORD"]
|
|
}
|
|
})
|
|
|
|
test("exportConfig writes an encrypted backup with the current or overridden password", async () => {
|
|
const state = createStoredState()
|
|
let exported: { path: string; password?: string } | null = null
|
|
|
|
const runtime = {
|
|
stateStore: {
|
|
hasUnlockedState: () => true,
|
|
getStateOrThrow: () => state,
|
|
getSnapshot: () => null,
|
|
exportStateFile: async (path: string, password?: string) => {
|
|
exported = { path, password }
|
|
},
|
|
importStateFile: async () => undefined,
|
|
rekey: async () => undefined,
|
|
upsertActualDomain: async () => undefined,
|
|
removeActualDomain: async () => undefined,
|
|
replaceActualDomains: async () => undefined
|
|
},
|
|
provisioning: {
|
|
getActivity: () => null
|
|
},
|
|
nodeDeployments: {
|
|
setReporter: () => undefined,
|
|
reinstallNode: async () => createTrackedServer(),
|
|
retryNode: async () => createTrackedServer(),
|
|
promoteNbdeNode: async () => createTrackedServer(),
|
|
reconcileNbde: async () => []
|
|
}
|
|
} as unknown as LegionEngine
|
|
|
|
process.env["LEGION_BACKUP_PASSWORD"] = "backup-password"
|
|
|
|
try {
|
|
const service = new NodeCliService(runtime)
|
|
const result = await service.exportConfig({
|
|
out: "/tmp/legion-backup.json",
|
|
passwordEnv: "LEGION_BACKUP_PASSWORD"
|
|
})
|
|
|
|
assert.deepEqual(exported, {
|
|
path: "/tmp/legion-backup.json",
|
|
password: "backup-password"
|
|
})
|
|
assert.equal(result.path, "/tmp/legion-backup.json")
|
|
assert.equal(result.config.name, "Tribe")
|
|
} finally {
|
|
delete process.env["LEGION_BACKUP_PASSWORD"]
|
|
}
|
|
})
|
|
|
|
test("importConfig restores an encrypted backup into the unlocked state store", async () => {
|
|
const state = createStoredState()
|
|
let imported: { path: string; password?: string } | null = null
|
|
|
|
const runtime = {
|
|
stateStore: {
|
|
hasUnlockedState: () => true,
|
|
getStateOrThrow: () => state,
|
|
getSnapshot: () => null,
|
|
exportStateFile: async () => undefined,
|
|
importStateFile: async (path: string, password?: string) => {
|
|
imported = { path, password }
|
|
},
|
|
rekey: async () => undefined,
|
|
upsertActualDomain: async () => undefined,
|
|
removeActualDomain: async () => undefined,
|
|
replaceActualDomains: async () => undefined
|
|
},
|
|
provisioning: {
|
|
getActivity: () => null
|
|
},
|
|
nodeDeployments: {
|
|
setReporter: () => undefined,
|
|
reinstallNode: async () => createTrackedServer(),
|
|
retryNode: async () => createTrackedServer(),
|
|
promoteNbdeNode: async () => createTrackedServer(),
|
|
reconcileNbde: async () => []
|
|
}
|
|
} as unknown as LegionEngine
|
|
|
|
const service = new NodeCliService(runtime)
|
|
const result = await service.importConfig({
|
|
in: "/tmp/legion-backup.json",
|
|
password: "backup-password"
|
|
})
|
|
|
|
assert.deepEqual(imported, {
|
|
path: "/tmp/legion-backup.json",
|
|
password: "backup-password"
|
|
})
|
|
assert.equal(result.path, "/tmp/legion-backup.json")
|
|
assert.equal(result.config.visibility, "invite_only")
|
|
})
|
|
|
|
test("addNode writes the desired server plan without materializing by default", async () => {
|
|
const state = createStoredState()
|
|
const { runtime, materializeCalls, updateSchemeCalls } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const result = await service.addNode({
|
|
name: "node-a",
|
|
providerId: "scaleway-e2e",
|
|
instance: "dev1-m",
|
|
diskGb: 80,
|
|
dnsHostname: "edge",
|
|
bootMode: "efi"
|
|
})
|
|
|
|
assert.equal(updateSchemeCalls.count, 1)
|
|
assert.equal(materializeCalls.count, 0)
|
|
assert.equal(state.scheme.servers.length, 1)
|
|
assert.deepEqual(state.scheme.servers[0]!.deploymentIntent, {
|
|
dnsHostname: "edge",
|
|
bootMode: "efi"
|
|
})
|
|
assert.equal(state.scheme.servers[0]!.instanceId, "scaleway-dev1-m")
|
|
assert.equal(state.scheme.servers[0]!.diskGb, 80)
|
|
assert.equal(result.node.id, "node-a")
|
|
assert.equal(result.node.provider, "scaleway")
|
|
assert.equal(state.trackedServers.length, 0)
|
|
})
|
|
|
|
test("addNode materializes the full scheme when requested", async () => {
|
|
const state = createStoredState()
|
|
const { runtime, materializeCalls } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const result = await service.addNode(
|
|
{
|
|
name: "node-a",
|
|
providerId: "scaleway-e2e",
|
|
instance: "dev1-m"
|
|
},
|
|
{ materialize: true }
|
|
)
|
|
|
|
assert.equal(materializeCalls.count, 1)
|
|
assert.equal(result.node.id, "node-a")
|
|
assert.equal(result.node.provider, "scaleway")
|
|
})
|
|
|
|
test("addNode stores a relative DNS hostname and derives domain DNS hosts", async () => {
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [],
|
|
domains: [
|
|
{
|
|
id: "domain-1",
|
|
domain: "example.test",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
source: "external",
|
|
delegatedZoneId: "zone-1",
|
|
label: "example.test"
|
|
}
|
|
],
|
|
dnsHosts: [],
|
|
dnsZones: [
|
|
{
|
|
id: "zone-1",
|
|
zoneName: "example.test",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
records: []
|
|
}
|
|
]
|
|
},
|
|
providers: [createProviderConfig(), createOvhProviderConfig()]
|
|
})
|
|
const { runtime } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
await service.addNode({
|
|
name: "node-a",
|
|
providerId: "scaleway-e2e",
|
|
instance: "dev1-m",
|
|
dnsHostname: "edge"
|
|
})
|
|
|
|
assert.deepEqual(state.scheme.dnsHosts, [])
|
|
assert.deepEqual(deriveDomainDnsHosts(state), [
|
|
{
|
|
id: "dns-host-node-a",
|
|
zoneId: "zone-1",
|
|
hostname: "edge.example.test",
|
|
serverIds: ["node-a"]
|
|
}
|
|
])
|
|
})
|
|
|
|
test("destroyNode removes the desired server from the scheme without materializing by default", async () => {
|
|
const existingPlan: ServerPlan = {
|
|
id: "node-a",
|
|
label: "node-a",
|
|
providerKind: "scaleway",
|
|
providerId: "scaleway-e2e",
|
|
source: "catalog",
|
|
instanceId: "scaleway-dev1-m",
|
|
planName: "DEV1-M",
|
|
region: "fr-par-1",
|
|
commercialMetadata: {
|
|
effectiveMonthlyGrossEur: 18,
|
|
source: "provider",
|
|
notes: []
|
|
},
|
|
cores: 3,
|
|
memoryGb: 4,
|
|
diskGb: 80,
|
|
role: "primary"
|
|
}
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [existingPlan],
|
|
domains: [],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
},
|
|
trackedServers: [createTrackedServer()]
|
|
})
|
|
const { runtime, materializeCalls, updateSchemeCalls } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const result = await service.destroyNode("node-a")
|
|
|
|
assert.equal(updateSchemeCalls.count, 1)
|
|
assert.equal(materializeCalls.count, 0)
|
|
assert.deepEqual(state.scheme.servers, [])
|
|
assert.equal(state.trackedServers.length, 1)
|
|
assert.equal(result.node.id, "node-a")
|
|
})
|
|
|
|
test("destroyNode materializes full deletion when requested", async () => {
|
|
const existingPlan: ServerPlan = {
|
|
id: "node-a",
|
|
label: "node-a",
|
|
providerKind: "scaleway",
|
|
providerId: "scaleway-e2e",
|
|
source: "catalog",
|
|
instanceId: "scaleway-dev1-m",
|
|
planName: "DEV1-M",
|
|
region: "fr-par-1",
|
|
commercialMetadata: {
|
|
effectiveMonthlyGrossEur: 18,
|
|
source: "provider",
|
|
notes: []
|
|
},
|
|
cores: 3,
|
|
memoryGb: 4,
|
|
diskGb: 80,
|
|
role: "primary"
|
|
}
|
|
const state = createStoredState({
|
|
scheme: {
|
|
servers: [existingPlan],
|
|
domains: [],
|
|
dnsHosts: [],
|
|
dnsZones: []
|
|
},
|
|
trackedServers: [createTrackedServer()]
|
|
})
|
|
const { runtime, materializeCalls } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
await service.destroyNode("node-a", { materialize: true })
|
|
|
|
assert.equal(materializeCalls.count, 1)
|
|
assert.deepEqual(state.trackedServers, [])
|
|
})
|
|
|
|
test("addDomain writes only the desired domain plan without materializing by default", async () => {
|
|
const state = createStoredState({
|
|
providers: [createProviderConfig(), createOvhProviderConfig()]
|
|
})
|
|
const { runtime, materializeCalls, updateSchemeCalls } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const result = await service.addDomain({
|
|
domain: "example.test",
|
|
provider: "ovh",
|
|
source: "managed",
|
|
registrantContact: {
|
|
firstName: "Ada",
|
|
lastName: "Lovelace",
|
|
email: "ada@example.test",
|
|
phone: "+49.30.555555",
|
|
address: {
|
|
line1: "Cloud Street 1",
|
|
city: "Berlin",
|
|
postalCode: "10115",
|
|
countryCode: "DE"
|
|
}
|
|
}
|
|
})
|
|
|
|
assert.equal(updateSchemeCalls.count, 1)
|
|
assert.equal(materializeCalls.count, 0)
|
|
assert.equal(state.scheme.domains.length, 1)
|
|
assert.equal(state.scheme.dnsZones.length, 0)
|
|
assert.equal(state.scheme.domains[0]?.domain, "example.test")
|
|
assert.equal(state.scheme.domains[0]?.providerKind, "ovh")
|
|
assert.equal(state.scheme.domains[0]?.autoRenew, true)
|
|
assert.equal(state.scheme.domains[0]?.registrantContact?.email, "ada@example.test")
|
|
assert.equal(result.domain.provider, "ovh")
|
|
assert.equal(result.domain.zoneId, undefined)
|
|
})
|
|
|
|
test("listDomains includes current registrar and delegation status when available", () => {
|
|
const state = createStoredState({
|
|
providers: [createProviderConfig(), createOvhProviderConfig()],
|
|
scheme: {
|
|
servers: [],
|
|
domains: [
|
|
{
|
|
id: "domain-1",
|
|
domain: "example.test",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
source: "managed",
|
|
delegatedZoneId: "zone-1",
|
|
label: "example.test",
|
|
autoRenew: true
|
|
}
|
|
],
|
|
dnsHosts: [],
|
|
dnsZones: [
|
|
{
|
|
id: "zone-1",
|
|
zoneName: "example.test",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
records: []
|
|
}
|
|
]
|
|
},
|
|
bindings: [
|
|
{
|
|
resourceType: "domain",
|
|
localId: "domain-1",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
remoteId: "example.test",
|
|
remoteName: "example.test",
|
|
matchSource: "name",
|
|
status: "managed",
|
|
lastSeenAt: "2026-04-10T00:00:00.000Z",
|
|
createdAt: "2026-04-10T00:00:00.000Z",
|
|
updatedAt: "2026-04-10T00:00:00.000Z"
|
|
},
|
|
{
|
|
resourceType: "dns-zone",
|
|
localId: "zone-1",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
remoteId: "example.test",
|
|
remoteName: "example.test",
|
|
matchSource: "name",
|
|
status: "managed",
|
|
lastSeenAt: "2026-04-10T00:00:00.000Z",
|
|
createdAt: "2026-04-10T00:00:00.000Z",
|
|
updatedAt: "2026-04-10T00:00:00.000Z"
|
|
}
|
|
],
|
|
actual: {
|
|
servers: [],
|
|
domains: [
|
|
{
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
remoteId: "example.test",
|
|
remoteName: "example.test",
|
|
domainName: "example.test",
|
|
autoRenew: true,
|
|
nameServers: ["dns10.ovh.net.", "ns10.ovh.net."],
|
|
dnsMode: "hosted",
|
|
observedAt: "2026-04-10T00:00:00.000Z",
|
|
matchSource: "name"
|
|
}
|
|
],
|
|
dnsZones: [
|
|
{
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
remoteId: "example.test",
|
|
remoteName: "example.test",
|
|
zoneName: "example.test",
|
|
records: [
|
|
{
|
|
id: "ns-root",
|
|
providerKind: "ovh",
|
|
zoneName: "example.test",
|
|
name: "@",
|
|
type: "NS",
|
|
values: [{ value: "ns10.ovh.net." }, { value: "dns10.ovh.net." }],
|
|
observedAt: "2026-04-10T00:00:00.000Z"
|
|
}
|
|
],
|
|
observedAt: "2026-04-10T00:00:00.000Z",
|
|
matchSource: "name"
|
|
}
|
|
],
|
|
refreshedAt: "2026-04-10T00:00:00.000Z"
|
|
}
|
|
})
|
|
const { runtime } = createRuntime(state)
|
|
|
|
const service = new NodeCliService(runtime)
|
|
const [domain] = service.listDomains()
|
|
|
|
assert.equal(domain?.registrationStatus, "registered")
|
|
assert.equal(domain?.delegationStatus, "hosted")
|
|
assert.deepEqual(domain?.desiredNameServers, ["dns10.ovh.net", "ns10.ovh.net"])
|
|
assert.deepEqual(domain?.observedNameServers, ["dns10.ovh.net", "ns10.ovh.net"])
|
|
assert.equal(domain?.bound, true)
|
|
})
|
|
|
|
test("destroyDomain removes the domain plan and leaves the zone plan intact", async () => {
|
|
const existingDomain: DomainPlan = {
|
|
id: "domain-1",
|
|
domain: "example.test",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
source: "managed",
|
|
delegatedZoneId: "dns-domain-1",
|
|
label: "example.test",
|
|
autoRenew: true,
|
|
registrantContact: {
|
|
firstName: "Ada",
|
|
lastName: "Lovelace",
|
|
email: "ada@example.test",
|
|
phone: "+49.30.555555",
|
|
address: {
|
|
line1: "Cloud Street 1",
|
|
city: "Berlin",
|
|
postalCode: "10115",
|
|
countryCode: "DE"
|
|
}
|
|
}
|
|
}
|
|
const state = createStoredState({
|
|
providers: [createProviderConfig(), createOvhProviderConfig()],
|
|
scheme: {
|
|
servers: [],
|
|
domains: [existingDomain],
|
|
dnsHosts: [],
|
|
dnsZones: [
|
|
{
|
|
id: "dns-domain-1",
|
|
zoneName: "example.test",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
records: []
|
|
}
|
|
]
|
|
}
|
|
})
|
|
const { runtime, materializeCalls, updateSchemeCalls } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const result = await service.destroyDomain("example.test")
|
|
|
|
assert.equal(updateSchemeCalls.count, 1)
|
|
assert.equal(materializeCalls.count, 0)
|
|
assert.equal(state.scheme.domains.length, 0)
|
|
assert.equal(state.scheme.dnsZones.length, 1)
|
|
assert.equal(result.domain.id, "domain-1")
|
|
assert.equal(result.domain.zoneId, "dns-domain-1")
|
|
})
|
|
|
|
test("getDomainNameServers returns authoritative apex NS records from the managed zone", async () => {
|
|
const existingDomain: DomainPlan = {
|
|
id: "domain-1",
|
|
domain: "example.test",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
source: "external",
|
|
delegatedZoneId: "dns-domain-1",
|
|
label: "example.test"
|
|
}
|
|
const state = createStoredState({
|
|
providers: [createProviderConfig(), createOvhProviderConfig()],
|
|
scheme: {
|
|
servers: [],
|
|
domains: [existingDomain],
|
|
dnsHosts: [],
|
|
dnsZones: [
|
|
{
|
|
id: "dns-domain-1",
|
|
zoneName: "example.test",
|
|
providerKind: "ovh",
|
|
providerId: "ovh",
|
|
records: []
|
|
}
|
|
]
|
|
}
|
|
})
|
|
const { runtime } = createRuntime(state)
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const result = await service.getDomainNameServers("example.test")
|
|
|
|
assert.equal(result.zoneName, "example.test")
|
|
assert.equal(result.source, "zone-records")
|
|
assert.deepEqual(result.nameServers, ["dns10.ovh.net", "ns10.ovh.net"])
|
|
})
|
|
|
|
test("quoteDomain returns OVH registration offers for a concrete domain", async () => {
|
|
const state = createStoredState({
|
|
providers: [createProviderConfig(), createOvhProviderConfig()]
|
|
})
|
|
const { runtime } = createRuntime(state)
|
|
const captured: Array<Record<string, unknown>> = []
|
|
runtime.quoteDomain = async (request) => {
|
|
captured.push(request as unknown as Record<string, unknown>)
|
|
return {
|
|
domain: "example.test",
|
|
provider: "ovh",
|
|
providerId: "ovh",
|
|
available: true,
|
|
source: "ovh-order-api",
|
|
offers: [
|
|
{
|
|
months: 12,
|
|
duration: "P1Y",
|
|
currency: "EUR",
|
|
totalGrossEur: 12,
|
|
monthlyGrossEur: 1,
|
|
planCode: "com",
|
|
pricingMode: "create-default",
|
|
offerId: undefined
|
|
}
|
|
]
|
|
}
|
|
}
|
|
const service = new NodeCliService(runtime)
|
|
|
|
const result = await service.quoteDomain({
|
|
domain: "example.test",
|
|
provider: "ovh",
|
|
providerId: "ovh"
|
|
})
|
|
|
|
assert.deepEqual(captured, [
|
|
{
|
|
domain: "example.test",
|
|
provider: "ovh",
|
|
providerId: "ovh"
|
|
}
|
|
])
|
|
assert.equal(result.provider, "ovh")
|
|
assert.equal(result.source, "ovh-order-api")
|
|
assert.equal(result.available, true)
|
|
assert.deepEqual(result.offers, [
|
|
{
|
|
months: 12,
|
|
duration: "P1Y",
|
|
currency: "EUR",
|
|
totalGrossEur: 12,
|
|
monthlyGrossEur: 1,
|
|
planCode: "com",
|
|
pricingMode: "create-default",
|
|
offerId: undefined
|
|
}
|
|
])
|
|
})
|