import { expect } from "@std/expect"; import { join } from "@std/path"; import { describe, it as test } from "@std/testing/bdd"; import { parseSnapshotArgs, readSnapshotRequest, readSnapshotRequestFromEnvFile, SNAPSHOT_USAGE, } from "#scripts/database-snapshot-lib.ts"; import { withEnv } from "#test-utils/env.ts"; import { withTempDir } from "#test-utils/files.ts"; const envReader = (values: Record) => (key: string): string | undefined => values[key]; describe("database snapshot options", () => { test("accepts one output path", () => { expect(parseSnapshotArgs(["--out", "backups/site.sqlite"])).toEqual({ outputPath: "backups/site.sqlite", }); }); test("returns help without an output path", () => { expect(parseSnapshotArgs(["--help"])).toBeNull(); expect(parseSnapshotArgs(["-h"])).toBeNull(); }); test("rejects missing, blank, duplicate, and extra output arguments", () => { const invalidArguments = [ [], ["--out"], ["--out", " "], ["--out", "one.sqlite", "--out", "two.sqlite"], ["--out", "site.sqlite", "extra"], ["--out", "site.sqlite", "--unknown"], ["--help", "--out", "site.sqlite"], ]; for (const args of invalidArguments) { expect(() => parseSnapshotArgs(args)).toThrow(SNAPSHOT_USAGE); } }); test("reads remote database credentials", () => { using _env = withEnv({ DB_TOKEN: "secret-token", DB_URL: "libsql://site.example.com", }); expect(readSnapshotRequest({ outputPath: "site.sqlite" })).toEqual({ dbToken: "secret-token", dbUrl: "libsql://site.example.com", outputPath: "site.sqlite", }); }); test("prefers database credentials from .env over shell values", () => withTempDir(async (dir) => { const envPath = join(dir, ".env"); await Deno.writeTextFile( envPath, 'DB_URL="libsql://file.example.com"\nDB_TOKEN="file-token"\n', ); await expect( readSnapshotRequestFromEnvFile( { outputPath: "site.sqlite" }, envPath, envReader({ DB_TOKEN: "shell-token", DB_URL: ":memory:" }), ), ).resolves.toEqual({ dbToken: "file-token", dbUrl: "libsql://file.example.com", outputPath: "site.sqlite", }); })); test("uses shell credentials when .env is absent", () => withTempDir( async (dir) => await expect( readSnapshotRequestFromEnvFile( { outputPath: "site.sqlite" }, join(dir, ".env"), envReader({ DB_TOKEN: "shell-token", DB_URL: "libsql://shell.example.com", }), ), ).resolves.toEqual({ dbToken: "shell-token", dbUrl: "libsql://shell.example.com", outputPath: "site.sqlite", }), )); test("rejects missing database credentials", () => { expect(() => readSnapshotRequest( { outputPath: "site.sqlite" }, envReader({ DB_TOKEN: "token" }), ), ).toThrow("DB_URL environment variable is required"); expect(() => readSnapshotRequest( { outputPath: "site.sqlite" }, envReader({ DB_URL: "libsql://site.example.com" }), ), ).toThrow("DB_TOKEN environment variable is required"); expect(() => readSnapshotRequest( { outputPath: "site.sqlite" }, envReader({ DB_TOKEN: " ", DB_URL: "libsql://site.example.com" }), ), ).toThrow("DB_TOKEN environment variable is required"); }); test("rejects local, insecure, and malformed database URLs", () => { for (const dbUrl of [ ":memory:", "file:site.sqlite", "http://site.example.com", "http://localhost.example.com", "libsql://site.example.com:8080?tls=0", "libsql://site.example.com:8080?tls=1&tls=0", "not a URL", ]) { expect(() => readSnapshotRequest( { outputPath: "site.sqlite" }, envReader({ DB_TOKEN: "token", DB_URL: dbUrl }), ), ).toThrow( "DB_URL must use TLS. Plain connections are allowed only for loopback.", ); } }); test("accepts each supported remote database protocol", () => { for (const dbUrl of [ "libsql://site.example.com", "libsql://site.example.com:8080?tls=0&tls=1", "https://site.example.com", "http://localhost:8080", "http://127.0.0.1:8080", "http://[::1]:8080", "libsql://localhost:8080?tls=0", "libsql://127.0.0.1:8080?tls=0", "libsql://[::1]:8080?tls=0", ]) { expect( readSnapshotRequest( { outputPath: "site.sqlite" }, envReader({ DB_TOKEN: "token", DB_URL: dbUrl }), ).dbUrl, ).toBe(dbUrl); } }); });