import assert from "node:assert/strict"; import { existsSync, mkdtempSync, statSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { NocoBaseAuthService } from "../src/auth-service.js"; import { loadStoredCredentials, saveCredentials } from "../src/credentials.js"; import type { OAuthCredentials } from "../src/oauth.js"; const oauthCredentials: OAuthCredentials = { type: "oauth", issuer: "https://justconn.top/api", clientId: "desktop-client", accessToken: "private-access-token", refreshToken: "private-refresh-token", tokenType: "Bearer", scope: "api offline_access", expiresAt: 901_000, }; test("returns a browser link immediately, then verifies and saves OAuth credentials in the background", async () => { const directory = mkdtempSync(join(tmpdir(), "nocobase-mcp-auth-service-")); const path = join(directory, "private", "credentials.json"); let resolveCredentials: (credentials: OAuthCredentials) => void = () => undefined; const credentials = new Promise((resolve) => { resolveCredentials = resolve; }); let verified = 0; let reloaded = 0; const service = new NocoBaseAuthService({ credentialsFile: path, now: () => 1_000, startLogin: async () => ({ prompt: { opened: false, url: "https://justconn.top/idpOAuth/device?user_code=ABCD-EFGH", userCode: "ABCD-EFGH", expiresIn: 600, }, credentials, }), verifyCredentials: async (value) => { assert.equal(value.accessToken, oauthCredentials.accessToken); verified += 1; }, onCredentialsSaved: () => { reloaded += 1; }, }); const started = await service.start(); assert.deepEqual(started, { status: "authorization_pending", login_url: "https://justconn.top/idpOAuth/device?user_code=ABCD-EFGH", user_code: "ABCD-EFGH", expires_in_seconds: 600, browser_opened: false, next_action: "Send login_url and user_code to the user. After approval, poll nocobase_login_status until authenticated.", }); assert.doesNotMatch(JSON.stringify(started), /private-access-token|private-refresh-token/); assert.equal(existsSync(path), false); assert.equal((await service.status()).status, "authorization_pending"); resolveCredentials(oauthCredentials); for (let attempt = 0; attempt < 10 && !existsSync(path); attempt += 1) { await new Promise((resolve) => setImmediate(resolve)); } assert.equal(verified, 1); assert.equal(reloaded, 1); assert.equal((await service.status()).status, "authenticated"); const stored = loadStoredCredentials(path); assert.equal(stored.type, "oauth"); if (process.platform !== "win32") assert.equal(statSync(path).mode & 0o777, 0o600); }); test("reuses stored login unless a fresh authorization is explicitly requested", async () => { const directory = mkdtempSync(join(tmpdir(), "nocobase-mcp-auth-force-")); const path = join(directory, "private", "credentials.json"); saveCredentials({ authorizeKey: "personal-key" }, path); let starts = 0; const service = new NocoBaseAuthService({ credentialsFile: path, startLogin: async () => { starts += 1; return { prompt: { opened: true, url: "https://justconn.top/idpOAuth/device?user_code=FRESH-CODE", userCode: "FRESH-CODE", expiresIn: 600, }, credentials: new Promise(() => undefined), }; }, }); assert.equal((await service.start()).status, "authenticated"); assert.equal(starts, 0); assert.equal((await service.start({ force: true })).status, "authorization_pending"); assert.equal(starts, 1); assert.equal((await service.start({ force: true })).status, "authorization_pending"); assert.equal(starts, 1); });