blob: af38aee089f558c26fe42281215555532572a4dc [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// Tests for the session-persistence helpers in auth.js (loadSession,
// clearSession). The session file lives at ~/.ponymail-mcp/session.json,
// computed at module import time from os.homedir(), so each test spawns a
// child node process with HOME pointed at a temporary directory.
//
// The interactive performLogin() flow (browser open, local HTTP server,
// cookie-paste form, network validation) is not covered by these tests —
// it requires a real browser and network and belongs in an integration
// suite.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
const here = path.dirname(fileURLToPath(import.meta.url));
const modulePath = path.join(here, "auth.js");
function withTempHome(fn) {
const home = mkdtempSync(path.join(tmpdir(), "ponymail-auth-test-"));
try {
return fn(home);
} finally {
rmSync(home, { recursive: true, force: true });
}
}
function runInChild(home, snippet, extraEnv = {}) {
const code = `
const a = await import(${JSON.stringify(modulePath)});
const out = await (async () => { ${snippet} })();
process.stdout.write(JSON.stringify(out));
`;
const baseEnv = { ...process.env, HOME: home, USERPROFILE: home };
// Always strip the opt-in so individual tests can re-set it explicitly.
delete baseEnv.PONYMAIL_AUTO_EXTRACT_COOKIE;
const res = spawnSync("node", ["--input-type=module", "-e", code], {
env: { ...baseEnv, ...extraEnv },
encoding: "utf8",
});
if (res.status !== 0) {
throw new Error(`child failed: ${res.stderr}`);
}
return JSON.parse(res.stdout);
}
function writeSessionFile(home, payload) {
const dir = path.join(home, ".ponymail-mcp");
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, "session.json"), JSON.stringify(payload));
}
test("loadSession returns null when no session file exists", () => {
withTempHome((home) => {
const out = runInChild(home, `return a.loadSession();`);
assert.equal(out, null);
});
});
test("loadSession returns the cookie when the file is fresh", () => {
withTempHome((home) => {
writeSessionFile(home, {
cookie: "ponymail=abc123",
timestamp: Date.now(),
user: { fullname: "Test" },
});
const out = runInChild(home, `return a.loadSession();`);
assert.equal(out, "ponymail=abc123");
});
});
test("loadSession returns null when the session is older than 20 hours", () => {
withTempHome((home) => {
const TWENTY_ONE_HOURS = 21 * 60 * 60 * 1000;
writeSessionFile(home, {
cookie: "ponymail=stale",
timestamp: Date.now() - TWENTY_ONE_HOURS,
});
const out = runInChild(home, `return a.loadSession();`);
assert.equal(out, null);
});
});
test("loadSession returns the cookie when there is no timestamp at all", () => {
// Behaviour today: missing timestamp skips the expiry check.
withTempHome((home) => {
writeSessionFile(home, { cookie: "ponymail=untimestamped" });
const out = runInChild(home, `return a.loadSession();`);
assert.equal(out, "ponymail=untimestamped");
});
});
test("loadSession returns null when the file is malformed JSON", () => {
withTempHome((home) => {
const dir = path.join(home, ".ponymail-mcp");
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, "session.json"), "{ not json");
const out = runInChild(home, `return a.loadSession();`);
assert.equal(out, null);
});
});
test("loadSession returns null when the cookie field is missing", () => {
withTempHome((home) => {
writeSessionFile(home, { timestamp: Date.now() });
const out = runInChild(home, `return a.loadSession();`);
assert.equal(out, null);
});
});
test("clearSession removes an existing session file", () => {
withTempHome((home) => {
writeSessionFile(home, { cookie: "ponymail=x", timestamp: Date.now() });
const sessionFile = path.join(home, ".ponymail-mcp", "session.json");
assert.equal(existsSync(sessionFile), true, "precondition: file exists");
runInChild(home, `a.clearSession(); return null;`);
assert.equal(existsSync(sessionFile), false, "session file should be deleted");
});
});
test("clearSession is a no-op when no session file exists", () => {
withTempHome((home) => {
// Should not throw.
const out = runInChild(home, `a.clearSession(); return "ok";`);
assert.equal(out, "ok");
});
});
// ---------------------------------------------------------------------------
// Auto-extract opt-in gate
// ---------------------------------------------------------------------------
test("autoExtractEnabled is false by default (env var unset)", () => {
withTempHome((home) => {
const out = runInChild(home, `return a.autoExtractEnabled();`);
assert.equal(out, false);
});
});
test("autoExtractEnabled is false for empty / falsy env var values", () => {
withTempHome((home) => {
for (const v of ["", "0", "no", "false", "off", "anything-else"]) {
const out = runInChild(home, `return a.autoExtractEnabled();`, {
PONYMAIL_AUTO_EXTRACT_COOKIE: v,
});
assert.equal(out, false, `expected false for env value ${JSON.stringify(v)}`);
}
});
});
test("autoExtractEnabled is true for the four accepted opt-in values", () => {
withTempHome((home) => {
for (const v of ["1", "true", "yes", "on", "TRUE", " 1 "]) {
const out = runInChild(home, `return a.autoExtractEnabled();`, {
PONYMAIL_AUTO_EXTRACT_COOKIE: v,
});
assert.equal(out, true, `expected true for env value ${JSON.stringify(v)}`);
}
});
});
// ---------------------------------------------------------------------------
// Smart paste extraction
// ---------------------------------------------------------------------------
function extract(input) {
return withTempHome((home) =>
runInChild(home, `return a.extractPonymailFromPaste(${JSON.stringify(input)});`)
);
}
test("extractPonymailFromPaste returns null for empty / whitespace input", () => {
assert.equal(extract(""), null);
assert.equal(extract(" \n "), null);
assert.equal(extract(null), null);
assert.equal(extract(undefined), null);
});
test("extractPonymailFromPaste accepts the raw ponymail=<value> token", () => {
assert.equal(
extract("ponymail=5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb"),
"ponymail=5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb"
);
});
test("extractPonymailFromPaste extracts from a full Cookie: header line", () => {
const input = "Cookie: lang=en; ponymail=5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb; _ga=GA1.1.42";
assert.equal(extract(input), "ponymail=5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb");
});
test("extractPonymailFromPaste extracts from a multi-line Request Headers paste", () => {
const input = [
"Host: lists.apache.org",
"User-Agent: Mozilla/5.0",
"Accept: application/json",
"Cookie: foo=bar; ponymail=5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb; baz=qux",
"Connection: keep-alive",
].join("\n");
assert.equal(extract(input), "ponymail=5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb");
});
test("extractPonymailFromPaste accepts a bare UUID (8-4-4-4-12)", () => {
assert.equal(
extract("5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb"),
"ponymail=5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb"
);
});
test("extractPonymailFromPaste is case-insensitive on the token name", () => {
assert.equal(
extract("Cookie: PonyMail=5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb;"),
"ponymail=5dc60945-f52a-4690-aaaa-bbbbbbbbbbbb"
);
});
test("extractPonymailFromPaste stops the value at the first separator", () => {
// The implementation breaks on whitespace, ;, ',', single-quote, double-quote
// — make sure trailing cookies / quotes don't leak into the value.
assert.equal(
extract('"ponymail=abc-123"; other=xx'),
"ponymail=abc-123"
);
assert.equal(
extract("ponymail=abc-123,other=xx"),
"ponymail=abc-123"
);
});
test("extractPonymailFromPaste returns null when no ponymail token is present", () => {
assert.equal(extract("Cookie: lang=en; _ga=GA1.1.42"), null);
assert.equal(extract("definitely not a cookie"), null);
});
test("extractPonymailFromPaste rejects a malformed bare value (not a UUID)", () => {
// We only accept bare UUIDs without the prefix; arbitrary strings shouldn't
// be silently wrapped in "ponymail=".
assert.equal(extract("not-a-uuid-shaped-string"), null);
});