Add multi-tenant cloud mode: self-serve purchase, credit metering, core-decoupling
Introduces RECAP_MODE=multi alongside single-mode self-host: - Tenant auth + accounts (magic-link via System SMTP), per-tenant credit pool, anonymous trial minting with per-IP/-64 caps - Self-serve Pro/Max purchase: inline Lightning (BTCPay) + card (Zaprite), prepaid 30-day periods, expiry-reminder emails - Core-decoupling: relay owns cloud tier/expiry keyed by Recaps user-id - SQLite (better-sqlite3) schema for multi-mode; filesystem unchanged for single - StartOS actions/versions through 0.2.155
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Tests for the ephemeral sessions the subscription background processor
|
||||
// mints to run /api/process AS each item's owner (per-tenant subscriptions,
|
||||
// step 4). The mechanism reuses the real sessions table, so verifying the
|
||||
// row it writes is valid + non-expired (and gets cleaned up) is enough to
|
||||
// trust the existing cookie → tenant-auth → req.user chain.
|
||||
|
||||
import { test, describe, before, after } from "node:test";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { initDb, getDb, closeDb } from "../db.js";
|
||||
import {
|
||||
mintInternalSession,
|
||||
deleteInternalSession,
|
||||
adminUserId,
|
||||
} from "../auth-routes.js";
|
||||
|
||||
let dataDir;
|
||||
|
||||
function makeUser({ id, email, isAdmin = 0 }) {
|
||||
getDb()
|
||||
.prepare(
|
||||
`INSERT INTO users (id, email, created_at, synthetic_install_id, is_admin)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(id, email, Date.now(), `inst-${id}`, isAdmin);
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
dataDir = mkdtempSync(path.join(tmpdir(), "recap-sess-"));
|
||||
await initDb({ dataDir });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
closeDb();
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("mintInternalSession / deleteInternalSession", () => {
|
||||
test("creates a valid, non-expired session row for the user", () => {
|
||||
makeUser({ id: "u-tenant", email: "tenant@example.com" });
|
||||
const token = mintInternalSession("u-tenant");
|
||||
assert.ok(typeof token === "string" && token.length > 20);
|
||||
|
||||
// Looks up exactly like tenant-auth does: by id, must be unexpired.
|
||||
const row = getDb()
|
||||
.prepare("SELECT * FROM sessions WHERE id = ? AND expires_at > ?")
|
||||
.get(token, Date.now());
|
||||
assert.ok(row, "session row exists and is not expired");
|
||||
assert.equal(row.user_id, "u-tenant");
|
||||
assert.ok(row.expires_at > Date.now(), "expires in the future");
|
||||
});
|
||||
|
||||
test("deleteInternalSession removes the row (no lingering identity)", () => {
|
||||
makeUser({ id: "u-temp", email: "temp@example.com" });
|
||||
const token = mintInternalSession("u-temp");
|
||||
assert.ok(getDb().prepare("SELECT 1 FROM sessions WHERE id = ?").get(token));
|
||||
deleteInternalSession(token);
|
||||
assert.equal(
|
||||
getDb().prepare("SELECT 1 FROM sessions WHERE id = ?").get(token),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("deleteInternalSession tolerates null / unknown tokens", () => {
|
||||
// Should not throw.
|
||||
deleteInternalSession(null);
|
||||
deleteInternalSession("does-not-exist");
|
||||
});
|
||||
});
|
||||
|
||||
describe("adminUserId", () => {
|
||||
test("returns the operator (is_admin = 1) user id", () => {
|
||||
makeUser({ id: "u-admin", email: "admin@example.com", isAdmin: 1 });
|
||||
assert.equal(adminUserId(), "u-admin");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user