Files
diegosouzapw--omniroute/tests/unit/kiro-windows-auto-import-3363.test.ts
2026-07-13 13:39:12 +08:00

192 lines
7.2 KiB
TypeScript

/**
* Regression guard for #3363 — Kiro auto-import failed on Windows because
* tryKiroCliSqlite() only probed the Linux/macOS path
* (~/.local/share/kiro-cli/data.sqlite3) and never checked the Kiro IDE
* path that Windows users have: %APPDATA%\kiro\storage.db
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// @ts-ignore — better-sqlite3 has no bundled types in this project
import Database from "better-sqlite3";
// Set DATA_DIR before importing any app modules so isAuthRequired() reads from
// a fresh, empty settings DB (no password → requireLogin defaults to false).
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-kiro-3363-data-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
// Import the GET handler at the module level so the DB is initialised once
// before any test runs.
const { GET } = await import("../../src/app/api/oauth/kiro/auto-import/route.ts");
const ORIGINAL_HOME = process.env.HOME;
const ORIGINAL_APPDATA = process.env.APPDATA;
const ORIGINAL_FETCH = globalThis.fetch;
let tmpHome: string;
test.beforeEach(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-kiro-3363-"));
// Reset DB instance so each test gets a clean settings DB (no requireLogin).
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
// Override HOME so homedir() returns a temp dir where no kiro-cli DB exists.
process.env.HOME = tmpHome;
// Ensure APPDATA is unset by default; individual tests that need it set it.
delete process.env.APPDATA;
});
test.afterEach(() => {
process.env.HOME = ORIGINAL_HOME;
if (ORIGINAL_APPDATA !== undefined) {
process.env.APPDATA = ORIGINAL_APPDATA;
} else {
delete process.env.APPDATA;
}
globalThis.fetch = ORIGINAL_FETCH;
if (tmpHome) fs.rmSync(tmpHome, { recursive: true, force: true });
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// Helper to call the GET handler and parse the JSON body.
async function callGet(): Promise<{ status: number; body: Record<string, unknown> }> {
const request = new Request("http://localhost/api/oauth/kiro/auto-import");
const response = await GET(request);
const body = (await response.json()) as Record<string, unknown>;
return { status: response.status, body };
}
test("triedPaths includes the Windows APPDATA path when process.env.APPDATA is set", async () => {
// Simulate a Windows environment with %APPDATA% pointing to a temp dir.
// The storage.db file does not exist there, so the probe fails gracefully.
process.env.APPDATA = tmpHome;
const { body } = await callGet();
assert.equal(body.found, false, "credentials must not be found when both DB files are absent");
assert.ok(Array.isArray(body.triedPaths), "triedPaths must be an array");
const expectedWindowsPath = path.join(tmpHome, "kiro", "storage.db");
assert.ok(
(body.triedPaths as string[]).includes(expectedWindowsPath),
`triedPaths must include the Windows APPDATA path ${expectedWindowsPath}, got: ${JSON.stringify(body.triedPaths)}`
);
});
test("triedPaths does NOT include any Windows path when process.env.APPDATA is not set", async () => {
// APPDATA is already unset by beforeEach.
const { body } = await callGet();
assert.equal(body.found, false, "credentials must not be found when DB file is absent");
assert.ok(Array.isArray(body.triedPaths), "triedPaths must be an array");
const paths = body.triedPaths as string[];
// No path should reference "kiro/storage.db" (the Windows IDE storage path).
const hasWindowsPath = paths.some(
(p) => p.includes("storage.db") && p.includes("kiro")
);
assert.equal(
hasWindowsPath,
false,
`triedPaths must not include any Windows kiro/storage.db path when APPDATA is unset, got: ${JSON.stringify(paths)}`
);
});
test("triedPaths always includes the Linux/macOS kiro-cli path", async () => {
const { body } = await callGet();
assert.ok(Array.isArray(body.triedPaths), "triedPaths must be an array");
const expectedLinuxPath = path.join(tmpHome, ".local/share/kiro-cli/data.sqlite3");
assert.ok(
(body.triedPaths as string[]).includes(expectedLinuxPath),
`triedPaths must always include the Linux/macOS kiro-cli path ${expectedLinuxPath}, got: ${JSON.stringify(body.triedPaths)}`
);
});
// ── Synthetic SQLite test: exercises the actual Windows schema-reading path ──
/**
* Creates a minimal storage.db in the Windows Kiro IDE schema:
* - Table: ItemTable (key TEXT, value TEXT)
* - Row: key="kiro:auth:token", value=JSON with refresh_token starting with "aorAAAAAG"
*
* Then sets APPDATA to the directory containing kiro/storage.db, calls GET,
* and asserts found:true with the correct refreshToken.
*/
test("GET extracts refresh_token from a Windows storage.db with ItemTable schema", async () => {
// Build the Windows storage.db directory structure: <tmpHome>/kiro/storage.db
const kiroDir = path.join(tmpHome, "kiro");
fs.mkdirSync(kiroDir, { recursive: true });
const dbPath = path.join(kiroDir, "storage.db");
// Populate the SQLite file using the Kiro IDE Windows schema.
const db = new Database(dbPath);
db.exec(`
CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT NOT NULL);
`);
const tokenValue = JSON.stringify({
refresh_token: "aorAAAAAGsynthetic-refresh-token",
access_token: "access-synthetic",
expires_at: new Date(Date.now() + 3600 * 1000).toISOString(),
region: "us-east-1",
});
db.prepare("INSERT INTO ItemTable (key, value) VALUES (?, ?)").run(
"kiro:auth:token",
tokenValue
);
db.close();
// Point APPDATA at tmpHome so tryKiroCliSqlite() resolves
// join(process.env.APPDATA, "kiro", "storage.db") to dbPath.
process.env.APPDATA = tmpHome;
// Mock fetch so the saveAndRespond path (kiroService.refreshToken()) succeeds
// without hitting the real Kiro OIDC endpoint.
globalThis.fetch = (async (url: string | URL | Request) => {
const u = String(url);
// registerClient() call — returns a new OIDC client registration.
if (u.includes("oidc.") && u.endsWith("/client/register")) {
return new Response(
JSON.stringify({ clientId: "reg-cid", clientSecret: "reg-secret", expiresIn: 86400 }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
// refreshToken() call — simulates Kiro OIDC token refresh.
if (u.includes("oidc.") && u.endsWith("/token")) {
return new Response(
JSON.stringify({
accessToken: "access-refreshed",
refreshToken: "aorAAAAAGrefreshed-token",
expiresIn: 3600,
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
throw new Error(`[kiro-3363 test] unexpected fetch to ${u}`);
}) as typeof fetch;
const { status, body } = await callGet();
assert.equal(
status,
200,
`expected HTTP 200, got ${status}: ${JSON.stringify(body)}`
);
assert.equal(
body.found,
true,
`expected found:true from Windows storage.db, got: ${JSON.stringify(body)}`
);
});