test(FN-2531): expand headless remote access test coverage

- Add serve command assertions for headless remote-access provider and lifecycle route behavior
- Add auth middleware integration coverage for hybrid login-url and remote-login token scenarios
- Add dedicated remote-access routes tests to verify route parity in headless mode
- Update dashboard route and server tests to validate remote auth and access flow expectations
This commit is contained in:
Fusion
2026-04-26 07:51:54 -07:00
committed by gsxdsm
parent a16a6a32d5
commit eba4022417
5 changed files with 313 additions and 5 deletions

View File

@@ -728,6 +728,22 @@ describe("runServe", () => {
await triggerSignal("SIGINT");
});
it("preserves remote-capable headless wiring when daemon auth is enabled", async () => {
const { createServer } = await import("@fusion/dashboard");
await runServe(0, { daemon: true });
expect(createServer).toHaveBeenCalledTimes(1);
const serverOptions = createServer.mock.calls[0][1];
expect(serverOptions).toMatchObject({ headless: true, daemon: { token: expect.any(String) } });
expect(serverOptions.daemon.token.length).toBeGreaterThan(0);
expect(serverOptions.engine).toBeDefined();
expect(typeof serverOptions.engine.startRemoteTunnel).toBe("function");
expect(typeof serverOptions.engine.stopRemoteTunnel).toBe("function");
await triggerSignal("SIGINT");
});
it("sets enginePaused when started with paused=true", async () => {
await runServe(0, { paused: true });

View File

@@ -281,4 +281,108 @@ describe("Auth middleware integration with createServer", () => {
expect(healthResponse.status).toBe(200);
});
});
describe("remote login hybrid token validation", () => {
function buildRemoteAccessSettings() {
return {
enabled: true,
activeProvider: "cloudflare",
providers: {
tailscale: {
enabled: false,
hostname: "tail.example.ts.net",
targetPort: 4040,
acceptRoutes: false,
},
cloudflare: {
enabled: true,
tunnelName: "demo-tunnel",
tunnelToken: "cf-secret",
ingressUrl: "https://remote.example.com",
},
},
tokenStrategy: {
persistent: {
enabled: true,
token: "frt_persistent_token",
},
shortLived: {
enabled: true,
ttlMs: 120000,
maxTtlMs: 86400000,
},
},
lifecycle: {
rememberLastRunning: false,
wasRunningOnShutdown: false,
lastRunningProvider: null,
},
};
}
it("returns fully-qualified login-url payloads and consistent invalid token errors", async () => {
vi.useFakeTimers();
try {
vi.setSystemTime(new Date("2026-04-26T12:00:00.000Z"));
const store = new MockStore() as unknown as TaskStore & { getSettings: ReturnType<typeof vi.fn> };
store.getSettings = vi.fn().mockResolvedValue({ remoteAccess: buildRemoteAccessSettings() });
const app = createServer(store as unknown as TaskStore, { daemon: { token: "fn_daemon_token" } });
const persistentIssue = await request(
app,
"POST",
"/api/remote-access/auth/login-url",
JSON.stringify({ mode: "persistent" }),
{
"Content-Type": "application/json",
Authorization: "Bearer fn_daemon_token",
},
);
expect(persistentIssue.status).toBe(200);
expect(persistentIssue.body).toMatchObject({ tokenType: "persistent" });
const persistentUrl = new URL(String((persistentIssue.body as Record<string, unknown>).loginUrl));
expect(persistentUrl.protocol).toBe("https:");
expect(persistentUrl.host).toBe("remote.example.com");
expect(persistentUrl.pathname).toBe("/remote-login");
expect(persistentUrl.searchParams.get("rt")).toMatch(/^frt_[A-Za-z0-9_-]+$/);
const shortIssue = await request(
app,
"POST",
"/api/remote-access/auth/login-url",
JSON.stringify({ mode: "short-lived" }),
{
"Content-Type": "application/json",
Authorization: "Bearer fn_daemon_token",
},
);
expect(shortIssue.status).toBe(200);
expect(shortIssue.body).toMatchObject({ tokenType: "short-lived", expiresAt: expect.any(String) });
const shortUrl = new URL(String((shortIssue.body as Record<string, unknown>).loginUrl));
const shortToken = shortUrl.searchParams.get("rt");
expect(shortToken).toMatch(/^frt_[A-Za-z0-9_-]+$/);
const invalid = await request(app, "GET", "/remote-login?rt=frt_wrong");
const missing = await request(app, "GET", "/remote-login");
vi.advanceTimersByTime(121000);
const expired = await request(app, "GET", `/remote-login?rt=${shortToken}`);
expect(invalid.status).toBe(401);
expect(missing.status).toBe(401);
expect(expired.status).toBe(401);
expect(invalid.body).toEqual({ error: "Unauthorized", code: "remote_token_invalid" });
expect(missing.body).toEqual({ error: "Unauthorized", code: "remote_token_missing" });
expect(expired.body).toEqual({ error: "Unauthorized", code: "remote_token_expired" });
} finally {
vi.useRealTimers();
}
});
});
});

View File

@@ -0,0 +1,161 @@
// @vitest-environment node
import { describe, expect, it, vi } from "vitest";
import express from "express";
import type { TaskStore } from "@fusion/core";
import { createApiRoutes } from "../routes.js";
import { request as performRequest } from "../test-request.js";
function buildRemoteAccessSettings(overrides: Record<string, unknown> = {}) {
return {
enabled: true,
activeProvider: "cloudflare" as const,
providers: {
tailscale: {
enabled: true,
hostname: "tail.example.ts.net",
targetPort: 4040,
acceptRoutes: false,
},
cloudflare: {
enabled: true,
tunnelName: "demo-tunnel",
tunnelToken: "cf-secret-token",
ingressUrl: "https://remote.example.com",
},
},
tokenStrategy: {
persistent: {
enabled: true,
token: "frt_persistent_token",
},
shortLived: {
enabled: true,
ttlMs: 120000,
maxTtlMs: 86400000,
},
},
lifecycle: {
rememberLastRunning: true,
wasRunningOnShutdown: false,
lastRunningProvider: null,
},
...overrides,
};
}
function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
return {
getSettings: vi.fn().mockResolvedValue({ remoteAccess: buildRemoteAccessSettings() }),
updateSettings: vi.fn(async (patch: Record<string, unknown>) => patch),
getRootDir: vi.fn().mockReturnValue("/fake/root"),
getFusionDir: vi.fn().mockReturnValue("/fake/root/.fusion"),
getDatabase: vi.fn().mockReturnValue({
prepare: vi.fn().mockReturnValue({ run: vi.fn(), get: vi.fn(), all: vi.fn() }),
exec: vi.fn(),
}),
listTasks: vi.fn().mockResolvedValue([]),
getTask: vi.fn(),
updateTask: vi.fn(),
moveTask: vi.fn(),
logEntry: vi.fn(),
getAgentLogs: vi.fn().mockResolvedValue([]),
on: vi.fn(),
off: vi.fn(),
...overrides,
} as unknown as TaskStore;
}
function createApp(opts: { store?: TaskStore; engine?: Record<string, unknown> } = {}) {
const store = opts.store ?? createMockStore();
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store, { engine: opts.engine as any }));
return { app, store };
}
async function REQUEST(app: express.Express, method: string, path: string, body?: unknown) {
return performRequest(
app,
method,
path,
body === undefined ? undefined : JSON.stringify(body),
body === undefined ? {} : { "Content-Type": "application/json" },
);
}
describe("remote access provider/lifecycle contracts", () => {
it("switches active provider and rejects invalid provider values", async () => {
const updateSettings = vi.fn().mockResolvedValue(undefined);
const { app } = createApp({ store: createMockStore({ updateSettings }) });
const activate = await REQUEST(app, "POST", "/api/remote/provider/activate", { provider: "tailscale" });
expect(activate.status).toBe(200);
expect(activate.body).toEqual({ activeProvider: "tailscale" });
expect(updateSettings).toHaveBeenCalledWith(expect.objectContaining({
remoteAccess: expect.objectContaining({ activeProvider: "tailscale" }),
}));
const invalid = await REQUEST(app, "POST", "/api/remote/provider/activate", { provider: "wireguard" });
expect(invalid.status).toBe(400);
expect(invalid.body).toEqual({
error: "Invalid remote provider",
details: { code: "INVALID_PROVIDER" },
});
});
it("returns NO_ACTIVE_PROVIDER when tunnel start is requested without an active provider", async () => {
const store = createMockStore({
getSettings: vi.fn().mockResolvedValue({
remoteAccess: buildRemoteAccessSettings({ activeProvider: null }),
}),
});
const { app } = createApp({ store });
const startRes = await REQUEST(app, "POST", "/api/remote/tunnel/start", {});
expect(startRes.status).toBe(409);
expect(startRes.body).toEqual({
error: "No active provider configured",
details: { code: "NO_ACTIVE_PROVIDER" },
});
});
it("keeps repeated start/stop requests idempotent when no engine is available", async () => {
const { app } = createApp();
const firstStart = await REQUEST(app, "POST", "/api/remote/tunnel/start", {});
const secondStart = await REQUEST(app, "POST", "/api/remote/tunnel/start", {});
const firstStop = await REQUEST(app, "POST", "/api/remote/tunnel/stop", {});
const secondStop = await REQUEST(app, "POST", "/api/remote/tunnel/stop", {});
for (const response of [firstStart, secondStart]) {
expect(response.status).toBe(200);
expect(response.body).toEqual({ state: "starting", provider: "cloudflare" });
expect(response.body).toEqual(expect.objectContaining({ state: expect.any(String), provider: expect.any(String) }));
}
for (const response of [firstStop, secondStop]) {
expect(response.status).toBe(200);
expect(response.body).toEqual({ state: "stopped", provider: "cloudflare" });
expect(response.body).toEqual(expect.objectContaining({ state: expect.any(String), provider: expect.any(String) }));
}
});
it("maps runtime prerequisite failures to a structured conflict response", async () => {
const store = createMockStore();
const engine = {
getTaskStore: vi.fn().mockReturnValue(store),
startRemoteTunnel: vi.fn().mockRejectedValue(new Error("runtime_prerequisite_missing:tailscale CLI unavailable")),
};
const { app } = createApp({ store, engine });
const response = await REQUEST(app, "POST", "/api/remote/tunnel/start", {});
expect(response.status).toBe(409);
expect(response.body).toEqual({
error: "tailscale CLI unavailable",
details: { code: "REMOTE_TUNNEL_PREREQUISITE_MISSING" },
});
});
});

View File

@@ -17926,7 +17926,12 @@ describe("remote access auth login-url endpoints", () => {
expect(res.status).toBe(200);
expect(res.body.tokenType).toBe("persistent");
expect(res.body.loginUrl).toContain("https://remote.example.com/remote-login?rt=");
const persistentLoginUrl = new URL(String(res.body.loginUrl));
expect(persistentLoginUrl.protocol).toBe("https:");
expect(persistentLoginUrl.host).toBe("remote.example.com");
expect(persistentLoginUrl.pathname).toBe("/remote-login");
expect(persistentLoginUrl.searchParams.get("rt")).toMatch(/^frt_[A-Za-z0-9_-]+$/);
expect(res.body.expiresAt).toBeUndefined();
expect(store.updateSettings).toHaveBeenCalledWith(expect.objectContaining({
remoteAccess: expect.objectContaining({
tokenStrategy: expect.objectContaining({
@@ -17965,7 +17970,12 @@ describe("remote access auth login-url endpoints", () => {
expect(res.status).toBe(200);
expect(res.body.tokenType).toBe("short-lived");
expect(res.body.expiresAt).toEqual(expect.any(String));
expect(res.body.loginUrl).toContain("/remote-login?rt=");
const shortLivedUrl = new URL(String(res.body.loginUrl));
expect(shortLivedUrl.protocol).toBe("https:");
expect(shortLivedUrl.host).toBe("remote.example.com");
expect(shortLivedUrl.pathname).toBe("/remote-login");
expect(shortLivedUrl.searchParams.get("rt")).toMatch(/^frt_[A-Za-z0-9_-]+$/);
expect(Date.parse(String(res.body.expiresAt))).not.toBeNaN();
expect(JSON.stringify(res.body)).not.toContain("cf-secret");
expect(JSON.stringify(res.body)).not.toContain("frt_persistent");
});
@@ -17981,5 +17991,6 @@ describe("remote access auth login-url endpoints", () => {
expect(res.status).toBe(400);
expect(res.body.error).toContain("mode must be");
expect(res.body.details).toEqual({ code: "INVALID_REMOTE_AUTH_MODE" });
});
});

View File

@@ -327,19 +327,35 @@ describe("createServer health and headless mode", () => {
const dashboard = createServer(createMockStore({ getSettings: vi.fn().mockResolvedValue({ remoteAccess }) }));
const headless = createServer(createMockStore({ getSettings: vi.fn().mockResolvedValue({ remoteAccess }) }), { headless: true });
const [dashSettings, headlessSettings, dashLoginUrl, headlessLoginUrl] = await Promise.all([
const [dashSettings, headlessSettings, dashLoginUrl, headlessLoginUrl, dashStatus, headlessStatus, headlessRoot] = await Promise.all([
GET(dashboard, "/api/remote/settings"),
GET(headless, "/api/remote/settings"),
REQUEST(headless, "POST", "/api/remote-access/auth/login-url", JSON.stringify({ mode: "persistent" }), { "Content-Type": "application/json" }),
REQUEST(dashboard, "POST", "/api/remote-access/auth/login-url", JSON.stringify({ mode: "persistent" }), { "Content-Type": "application/json" }),
GET(dashboard, "/api/remote/status"),
GET(headless, "/api/remote/status"),
GET(headless, "/"),
]);
expect(dashSettings.status).toBe(200);
expect(headlessSettings.status).toBe(200);
expect(dashLoginUrl.status).toBe(200);
expect(headlessLoginUrl.status).toBe(200);
expect(String((dashLoginUrl.body as Record<string, unknown>).loginUrl)).toContain("/remote-login?rt=");
expect(String((headlessLoginUrl.body as Record<string, unknown>).loginUrl)).toContain("/remote-login?rt=");
expect(dashStatus.status).toBe(200);
expect(headlessStatus.status).toBe(200);
const dashLoginBody = dashLoginUrl.body as Record<string, unknown>;
const headlessLoginBody = headlessLoginUrl.body as Record<string, unknown>;
expect(Object.keys(dashLoginBody).sort()).toEqual(["loginUrl", "tokenType"]);
expect(Object.keys(headlessLoginBody).sort()).toEqual(["loginUrl", "tokenType"]);
expect(String(dashLoginBody.loginUrl)).toContain("/remote-login?rt=");
expect(String(headlessLoginBody.loginUrl)).toContain("/remote-login?rt=");
const dashStatusBody = dashStatus.body as Record<string, unknown>;
const headlessStatusBody = headlessStatus.body as Record<string, unknown>;
expect(Object.keys(dashStatusBody).sort()).toEqual(["lastError", "lastErrorCode", "provider", "restore", "state", "url"]);
expect(Object.keys(headlessStatusBody).sort()).toEqual(["lastError", "lastErrorCode", "provider", "restore", "state", "url"]);
expect(headlessRoot.status).toBe(404);
});
});