mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-05-22 19:57:07 +00:00
Adds three input modes for importing multiple Codex accounts at once,
all feeding a single partial-failure backend endpoint.
- `codexAuthZipExtract.ts`: safe ZIP extraction via fflate — rejects
path traversal (../ and absolute paths), per-file 256 KB cap, 10 MB
total cap, max 50 .json entries
- `POST /api/providers/codex-auth/zip-extract`: server-side ZIP
extraction returning [{name, json, parseError}] to the client
- `POST /api/providers/codex-auth/import-bulk`: iterates entries,
partial-failure semantics (always 200), per-entry audit log
`provider.credentials.imported` + summary `bulk_imported`
- `importCodexAuthBulkSchema`: max 50 entries, email validation
- `<ImportCodexAuthModal>`: Single/Bulk top tabs; Bulk has Upload
files, Paste list (JSON array or --- separator), ZIP sub-modes;
live entry preview list; overwrite checkbox; result panel
- Install `fflate@0.8.3` (pure TypeScript, zero native deps)
- 29 unit tests: 12 ZIP safety cases + 17 schema/parser/shape cases
187 lines
6.1 KiB
TypeScript
187 lines
6.1 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { z } from "zod";
|
|
|
|
// Local copy of importCodexAuthBulkSchema — avoids importing Next.js deps.
|
|
const importCodexAuthBulkSchema = z.object({
|
|
entries: z
|
|
.array(
|
|
z.object({
|
|
json: z.unknown(),
|
|
name: z.string().min(1).max(200).optional(),
|
|
email: z.string().email().optional(),
|
|
})
|
|
)
|
|
.min(1)
|
|
.max(50),
|
|
overwriteExisting: z.boolean().optional(),
|
|
});
|
|
|
|
function parse(body: unknown) {
|
|
return importCodexAuthBulkSchema.safeParse(body);
|
|
}
|
|
|
|
// ──── Schema tests ────────────────────────────────────────────────────────────
|
|
|
|
test("bulk schema: valid single entry passes", () => {
|
|
const result = parse({ entries: [{ json: { auth_mode: "chatgpt" } }] });
|
|
assert.ok(result.success);
|
|
});
|
|
|
|
test("bulk schema: valid multiple entries pass", () => {
|
|
const result = parse({
|
|
entries: [
|
|
{ json: {}, name: "Account A", email: "a@example.com" },
|
|
{ json: {}, name: "Account B" },
|
|
],
|
|
overwriteExisting: true,
|
|
});
|
|
assert.ok(result.success);
|
|
assert.equal(result.data.entries.length, 2);
|
|
assert.equal(result.data.overwriteExisting, true);
|
|
});
|
|
|
|
test("bulk schema: empty entries array fails", () => {
|
|
const result = parse({ entries: [] });
|
|
assert.ok(!result.success);
|
|
});
|
|
|
|
test("bulk schema: missing entries fails", () => {
|
|
const result = parse({});
|
|
assert.ok(!result.success);
|
|
});
|
|
|
|
test("bulk schema: 50 entries passes", () => {
|
|
const entries = Array.from({ length: 50 }, (_, i) => ({ json: { index: i } }));
|
|
const result = parse({ entries });
|
|
assert.ok(result.success);
|
|
});
|
|
|
|
test("bulk schema: 51 entries fails", () => {
|
|
const entries = Array.from({ length: 51 }, (_, i) => ({ json: { index: i } }));
|
|
const result = parse({ entries });
|
|
assert.ok(!result.success);
|
|
});
|
|
|
|
test("bulk schema: invalid email in entry fails", () => {
|
|
const result = parse({
|
|
entries: [{ json: {}, email: "not-an-email" }],
|
|
});
|
|
assert.ok(!result.success);
|
|
const emailIssue = result.error.issues.find((i) => i.path.some((p) => p === "email"));
|
|
assert.ok(emailIssue);
|
|
});
|
|
|
|
test("bulk schema: empty name in entry fails", () => {
|
|
const result = parse({ entries: [{ json: {}, name: "" }] });
|
|
assert.ok(!result.success);
|
|
});
|
|
|
|
test("bulk schema: overwriteExisting must be boolean", () => {
|
|
const result = parse({
|
|
entries: [{ json: {} }],
|
|
overwriteExisting: "yes",
|
|
});
|
|
assert.ok(!result.success);
|
|
});
|
|
|
|
// ──── Paste-list parser tests ─────────────────────────────────────────────────
|
|
|
|
// Client-side paste parser logic (mirrors what the UI does before submitting)
|
|
function parsePasteList(text: string): Array<{ json: unknown; parseError: string | null }> {
|
|
const trimmed = text.trim();
|
|
if (!trimmed) return [];
|
|
|
|
// Try as a JSON array first
|
|
try {
|
|
const parsed = JSON.parse(trimmed);
|
|
if (Array.isArray(parsed)) {
|
|
return parsed.map((item) => ({ json: item, parseError: null }));
|
|
}
|
|
// Single object
|
|
return [{ json: parsed, parseError: null }];
|
|
} catch {
|
|
// Fall back to --- separator
|
|
const parts = trimmed
|
|
.split(/^---$/m)
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
return parts.map((part) => {
|
|
try {
|
|
return { json: JSON.parse(part), parseError: null };
|
|
} catch {
|
|
return { json: null, parseError: "Invalid JSON" };
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
test("paste parser: JSON array returns all items", () => {
|
|
const text = JSON.stringify([{ auth_mode: "chatgpt" }, { auth_mode: "chatgpt" }]);
|
|
const result = parsePasteList(text);
|
|
assert.equal(result.length, 2);
|
|
assert.ok(result.every((r) => r.parseError === null));
|
|
});
|
|
|
|
test("paste parser: single JSON object returns one item", () => {
|
|
const text = JSON.stringify({ auth_mode: "chatgpt" });
|
|
const result = parsePasteList(text);
|
|
assert.equal(result.length, 1);
|
|
assert.equal(result[0].parseError, null);
|
|
});
|
|
|
|
test("paste parser: --- separator splits multiple JSONs", () => {
|
|
const a = JSON.stringify({ auth_mode: "chatgpt", index: 1 });
|
|
const b = JSON.stringify({ auth_mode: "chatgpt", index: 2 });
|
|
const text = `${a}\n---\n${b}`;
|
|
const result = parsePasteList(text);
|
|
assert.equal(result.length, 2);
|
|
assert.ok(result.every((r) => r.parseError === null));
|
|
});
|
|
|
|
test("paste parser: invalid JSON in --- section marked as error", () => {
|
|
const good = JSON.stringify({ auth_mode: "chatgpt" });
|
|
const text = `${good}\n---\nnot valid json`;
|
|
const result = parsePasteList(text);
|
|
assert.equal(result.length, 2);
|
|
assert.equal(result[0].parseError, null);
|
|
assert.ok(result[1].parseError !== null);
|
|
});
|
|
|
|
test("paste parser: empty string returns empty array", () => {
|
|
assert.equal(parsePasteList("").length, 0);
|
|
assert.equal(parsePasteList(" ").length, 0);
|
|
});
|
|
|
|
// ──── Partial-failure response shape ──────────────────────────────────────────
|
|
|
|
test("bulk response shape: partial success has correct structure", () => {
|
|
const response = {
|
|
success: 2,
|
|
failed: 1,
|
|
total: 3,
|
|
created: [{ id: "a" }, { id: "b" }],
|
|
errors: [{ index: 2, name: "entry 3", message: "duplicate_account" }],
|
|
};
|
|
|
|
assert.equal(response.success + response.failed, response.total);
|
|
assert.equal(response.created.length, response.success);
|
|
assert.equal(response.errors.length, response.failed);
|
|
assert.ok("index" in response.errors[0]);
|
|
assert.ok("name" in response.errors[0]);
|
|
assert.ok("message" in response.errors[0]);
|
|
});
|
|
|
|
test("bulk response shape: all-failure has status failed", () => {
|
|
const entries = 3;
|
|
const failed = 3;
|
|
const status = failed === entries ? "failure" : "success";
|
|
assert.equal(status, "failure");
|
|
});
|
|
|
|
test("bulk response shape: partial failure has status success", () => {
|
|
const entries = 3;
|
|
const failed = 1;
|
|
const status = failed === entries ? "failure" : "success";
|
|
assert.equal(status, "success");
|
|
});
|