106 lines
3.8 KiB
TypeScript
106 lines
3.8 KiB
TypeScript
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<OAuthCredentials>((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<void>((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<OAuthCredentials>(() => 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);
|
|
});
|