diff --git a/key_share_node/docker/docker-compose.yml b/key_share_node/docker/docker-compose.yml index b79395993..9ed504d03 100644 --- a/key_share_node/docker/docker-compose.yml +++ b/key_share_node/docker/docker-compose.yml @@ -11,7 +11,6 @@ services: PGTZ: "UTC" volumes: - ${PG_DATA_DIR}:/var/lib/postgresql/data - - ../../key_share_node/pg_interface/src/bin/migrate/migrate.sql:/docker-entrypoint-initdb.d/migrate.sql key_share_node: build: diff --git a/key_share_node/pg_interface/migrations/20251215000000_add_auth_type_to_users.ts b/key_share_node/pg_interface/migrations/20251215000000_add_auth_type_to_users.ts deleted file mode 100644 index f6639174a..000000000 --- a/key_share_node/pg_interface/migrations/20251215000000_add_auth_type_to_users.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - const publicSchemaBuilder = knex.schema.withSchema("public"); - - // 1. Add auth_type column with default 'google' - await publicSchemaBuilder.alterTable("users", (table) => { - table - .string("auth_type", 64) - .notNullable() - .defaultTo("google") - .after("email"); - }); - - // 2. Drop the old unique constraint on email only - await knex.raw(` - ALTER TABLE public.users - DROP CONSTRAINT IF EXISTS users_email_key - `); - - // 3. Create new unique constraint on (email, auth_type) - await knex.raw(` - ALTER TABLE public.users - ADD CONSTRAINT users_email_auth_type_key UNIQUE (email, auth_type) - `); -} - -export async function down(knex: Knex): Promise { - // 1. Drop the new unique constraint on (email, auth_type) - await knex.raw(` - ALTER TABLE public.users - DROP CONSTRAINT IF EXISTS users_email_auth_type_key - `); - - // 2. Restore the old unique constraint on email only - await knex.raw(` - ALTER TABLE public.users - ADD CONSTRAINT users_email_key UNIQUE (email) - `); - - // 3. Drop the auth_type column - await knex.schema.withSchema("public").alterTable("users", (table) => { - table.dropColumn("auth_type"); - }); -} diff --git a/key_share_node/pg_interface/migrations/20251209125945_initial.ts b/key_share_node/pg_interface/migrations/20251223194800_initial.ts similarity index 58% rename from key_share_node/pg_interface/migrations/20251209125945_initial.ts rename to key_share_node/pg_interface/migrations/20251223194800_initial.ts index f73f19a62..078fcc5ab 100644 --- a/key_share_node/pg_interface/migrations/20251209125945_initial.ts +++ b/key_share_node/pg_interface/migrations/20251223194800_initial.ts @@ -2,85 +2,20 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { // - // public.key_shares + // public.2_users // - const keySharesExists = await knex.schema + const usersExists = await knex.schema .withSchema("public") - .hasTable("key_shares"); - if (!keySharesExists) { - await knex.schema - .withSchema("public") - .createTable("key_shares", (table) => { - table - .uuid("share_id") - .notNullable() - .defaultTo(knex.raw("gen_random_uuid()")) - .primary({ constraintName: "key_shares_pkey" }); - table.uuid("wallet_id").notNullable(); - table.binary("enc_share").notNullable(); - table.string("status").notNullable(); - table - .timestamp("reshared_at", { useTz: true }) - .notNullable() - .defaultTo(knex.fn.now()); - table - .timestamp("created_at", { useTz: true }) - .notNullable() - .defaultTo(knex.fn.now()); - table - .timestamp("updated_at", { useTz: true }) - .notNullable() - .defaultTo(knex.fn.now()); - table.jsonb("aux"); - - table.unique(["wallet_id"], { - indexName: "key_shares_unique", - }); - }); - } - - // - // public.pg_dumps - // - const pgDumpsExists = await knex.schema - .withSchema("public") - .hasTable("pg_dumps"); - if (!pgDumpsExists) { - await knex.schema.withSchema("public").createTable("pg_dumps", (table) => { - table - .uuid("dump_id") - .notNullable() - .defaultTo(knex.raw("gen_random_uuid()")) - .primary({ constraintName: "pg_dumps_pkey" }); - table.string("status", 16).notNullable(); - table.string("dump_path", 255); - table.jsonb("meta"); - table - .timestamp("created_at", { useTz: true }) - .notNullable() - .defaultTo(knex.fn.now()); - table - .timestamp("updated_at", { useTz: true }) - .notNullable() - .defaultTo(knex.fn.now()); - }); - } - - // - // public.users - // - const usersExists = await knex.schema.withSchema("public").hasTable("users"); + .hasTable("2_users"); if (!usersExists) { - await knex.schema.withSchema("public").createTable("users", (table) => { + await knex.schema.withSchema("public").createTable("2_users", (table) => { table .uuid("user_id") .notNullable() .defaultTo(knex.raw("gen_random_uuid()")) - .primary({ constraintName: "users_pkey" }); - table - .string("email", 255) - .notNullable() - .unique({ indexName: "users_email_key" }); + .primary({ constraintName: "2_users_pkey" }); + table.string("auth_type", 64).notNullable(); + table.string("email", 255).notNullable(); table.string("status", 16).notNullable().defaultTo("active"); table .timestamp("created_at", { useTz: true }) @@ -91,28 +26,32 @@ export async function up(knex: Knex): Promise { .notNullable() .defaultTo(knex.fn.now()); table.jsonb("aux"); + + table.unique(["auth_type", "email"], { + indexName: "2_users_auth_type_email_key", + }); }); } // - // public.wallets + // public.2_wallets // const walletsExists = await knex.schema .withSchema("public") - .hasTable("wallets"); + .hasTable("2_wallets"); if (!walletsExists) { - await knex.schema.withSchema("public").createTable("wallets", (table) => { + await knex.schema.withSchema("public").createTable("2_wallets", (table) => { table .uuid("wallet_id") .notNullable() .defaultTo(knex.raw("gen_random_uuid()")) - .primary({ constraintName: "wallets_pkey" }); + .primary({ constraintName: "2_wallets_pkey" }); table.uuid("user_id").notNullable(); table.string("curve_type", 16).notNullable(); table .binary("public_key") .notNullable() - .unique({ indexName: "wallets_public_key_key" }); + .unique({ indexName: "2_wallets_public_key_key" }); table .timestamp("created_at", { useTz: true }) .notNullable() @@ -126,24 +65,62 @@ export async function up(knex: Knex): Promise { } // - // public.server_keypairs + // public.2_key_shares + // + const keySharesExists = await knex.schema + .withSchema("public") + .hasTable("2_key_shares"); + if (!keySharesExists) { + await knex.schema + .withSchema("public") + .createTable("2_key_shares", (table) => { + table + .uuid("share_id") + .notNullable() + .defaultTo(knex.raw("gen_random_uuid()")) + .primary({ constraintName: "2_key_shares_pkey" }); + table.uuid("wallet_id").notNullable(); + table.binary("enc_share").notNullable(); + table.string("status").notNullable(); + table + .timestamp("reshared_at", { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + table + .timestamp("created_at", { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + table + .timestamp("updated_at", { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + table.jsonb("aux"); + + table.unique(["wallet_id"], { + indexName: "2_key_shares_unique", + }); + }); + } + + // + // public.2_server_keypairs // const serverKeypairsExists = await knex.schema .withSchema("public") - .hasTable("server_keypairs"); + .hasTable("2_server_keypairs"); if (!serverKeypairsExists) { await knex.schema .withSchema("public") - .createTable("server_keypairs", (table) => { + .createTable("2_server_keypairs", (table) => { table .uuid("keypair_id") .notNullable() .defaultTo(knex.raw("gen_random_uuid()")) - .primary({ constraintName: "server_keypairs_pkey" }); + .primary({ constraintName: "2_server_keypairs_pkey" }); table .specificType("version", "integer generated always as identity") .notNullable() - .unique({ indexName: "server_keypairs_version_key" }); + .unique({ indexName: "2_server_keypairs_version_key" }); table.binary("public_key").notNullable(); table.text("enc_private_key").notNullable(); table.boolean("is_active").notNullable().defaultTo(true); @@ -159,17 +136,46 @@ export async function up(knex: Knex): Promise { }); } + // + // public.2_pg_dumps + // + const pgDumpsExists = await knex.schema + .withSchema("public") + .hasTable("2_pg_dumps"); + if (!pgDumpsExists) { + await knex.schema + .withSchema("public") + .createTable("2_pg_dumps", (table) => { + table + .uuid("dump_id") + .notNullable() + .defaultTo(knex.raw("gen_random_uuid()")) + .primary({ constraintName: "2_pg_dumps_pkey" }); + table.string("status", 16).notNullable(); + table.string("dump_path", 255); + table.jsonb("meta"); + table + .timestamp("created_at", { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + table + .timestamp("updated_at", { useTz: true }) + .notNullable() + .defaultTo(knex.fn.now()); + }); + } + await knex.raw(` - CREATE INDEX IF NOT EXISTS idx_server_keypairs_is_active - ON public.server_keypairs (is_active) + CREATE INDEX IF NOT EXISTS idx_2_server_keypairs_is_active + ON public."2_server_keypairs" (is_active) WHERE is_active = true `); } export async function down(knex: Knex): Promise { - await knex.schema.withSchema("public").dropTableIfExists("server_keypairs"); - await knex.schema.withSchema("public").dropTableIfExists("wallets"); - await knex.schema.withSchema("public").dropTableIfExists("users"); - await knex.schema.withSchema("public").dropTableIfExists("pg_dumps"); - await knex.schema.withSchema("public").dropTableIfExists("key_shares"); + await knex.schema.withSchema("public").dropTableIfExists("2_users"); + await knex.schema.withSchema("public").dropTableIfExists("2_wallets"); + await knex.schema.withSchema("public").dropTableIfExists("2_key_shares"); + await knex.schema.withSchema("public").dropTableIfExists("2_server_keypairs"); + await knex.schema.withSchema("public").dropTableIfExists("2_pg_dumps"); } diff --git a/key_share_node/pg_interface/src/bin/migrate/migrate.sql b/key_share_node/pg_interface/src/bin/migrate/migrate.sql index bfea3a301..7d5a15220 100644 --- a/key_share_node/pg_interface/src/bin/migrate/migrate.sql +++ b/key_share_node/pg_interface/src/bin/migrate/migrate.sql @@ -1,10 +1,10 @@ --- public.key_shares definition +-- public.2_key_shares definition -- Drop table --- DROP TABLE public.key_shares; +-- DROP TABLE public."2_key_shares"; -CREATE TABLE public.key_shares ( +CREATE TABLE public."2_key_shares" ( share_id uuid DEFAULT gen_random_uuid() NOT NULL, wallet_id uuid NOT NULL, enc_share bytea NOT NULL, @@ -13,54 +13,54 @@ CREATE TABLE public.key_shares ( created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, aux jsonb NULL, - CONSTRAINT key_shares_pkey PRIMARY KEY (share_id), - CONSTRAINT key_shares_unique UNIQUE (wallet_id) + CONSTRAINT 2_key_shares_pkey PRIMARY KEY (share_id), + CONSTRAINT 2_key_shares_unique UNIQUE (wallet_id) ); --- public.pg_dumps definition +-- public.2_pg_dumps definition -- Drop table --- DROP TABLE public.pg_dumps; +-- DROP TABLE public."2_pg_dumps"; -CREATE TABLE public.pg_dumps ( +CREATE TABLE public."2_pg_dumps" ( dump_id uuid DEFAULT gen_random_uuid() NOT NULL, status varchar(16) NOT NULL, dump_path varchar(255) NULL, meta jsonb NULL, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, - CONSTRAINT pg_dumps_pkey PRIMARY KEY (dump_id) + CONSTRAINT 2_pg_dumps_pkey PRIMARY KEY (dump_id) ); --- public.users definition +-- public.2_users definition -- Drop table --- DROP TABLE public.users; +-- DROP TABLE public."2_users"; -CREATE TABLE public.users ( +CREATE TABLE public."2_users" ( user_id uuid DEFAULT gen_random_uuid() NOT NULL, + auth_type varchar(64) NOT NULL, email varchar(255) NOT NULL, - auth_type varchar(64) DEFAULT 'google'::character varying NOT NULL, status varchar(16) DEFAULT 'active'::character varying NOT NULL, created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, aux jsonb NULL, - CONSTRAINT users_email_auth_type_key UNIQUE (email, auth_type), - CONSTRAINT users_pkey PRIMARY KEY (user_id) + CONSTRAINT 2_users_pkey PRIMARY KEY (user_id), + CONSTRAINT 2_users_auth_type_email_key UNIQUE (auth_type, email) ); --- public.wallets definition +-- public.2_wallets definition -- Drop table --- DROP TABLE public.wallets; +-- DROP TABLE public."2_wallets"; -CREATE TABLE public.wallets ( +CREATE TABLE public."2_wallets" ( wallet_id uuid DEFAULT gen_random_uuid() NOT NULL, user_id uuid NOT NULL, curve_type varchar(16) NOT NULL, @@ -68,18 +68,18 @@ CREATE TABLE public.wallets ( created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, aux jsonb NULL, - CONSTRAINT wallets_pkey PRIMARY KEY (wallet_id), - CONSTRAINT wallets_public_key_key UNIQUE (public_key) + CONSTRAINT 2_wallets_pkey PRIMARY KEY (wallet_id), + CONSTRAINT 2_wallets_public_key_key UNIQUE (public_key) ); --- public.server_keypairs definition +-- public.2_server_keypairs definition -- Drop table --- DROP TABLE public.server_keypairs; +-- DROP TABLE public."2_server_keypairs"; -CREATE TABLE public.server_keypairs ( +CREATE TABLE public."2_server_keypairs" ( keypair_id uuid DEFAULT gen_random_uuid() NOT NULL, version int4 GENERATED ALWAYS AS IDENTITY NOT NULL, public_key bytea NOT NULL, @@ -88,7 +88,7 @@ CREATE TABLE public.server_keypairs ( created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, rotated_at timestamptz NULL, - CONSTRAINT server_keypairs_pkey PRIMARY KEY (keypair_id), - CONSTRAINT server_keypairs_version_key UNIQUE (version) + CONSTRAINT 2_server_keypairs_pkey PRIMARY KEY (keypair_id), + CONSTRAINT 2_server_keypairs_version_key UNIQUE (version) ); -CREATE INDEX idx_server_keypairs_is_active ON public.server_keypairs USING btree (is_active) WHERE (is_active = true); +CREATE INDEX idx_2_server_keypairs_is_active ON public."2_server_keypairs" USING btree (is_active) WHERE (is_active = true); diff --git a/key_share_node/pg_interface/src/key_shares/index.ts b/key_share_node/pg_interface/src/key_shares/index.ts index f1aa5fa90..bf60ba078 100644 --- a/key_share_node/pg_interface/src/key_shares/index.ts +++ b/key_share_node/pg_interface/src/key_shares/index.ts @@ -13,7 +13,7 @@ export async function createKeyShare( ): Promise> { try { const query = ` -INSERT INTO key_shares ( +INSERT INTO "2_key_shares" ( share_id, wallet_id, enc_share, status ) VALUES ( @@ -48,7 +48,7 @@ export async function getKeyShareByShareId( ): Promise> { try { const query = ` -SELECT * FROM key_shares +SELECT * FROM "2_key_shares" WHERE share_id = $1 LIMIT 1 `; @@ -71,7 +71,7 @@ export async function getKeyShareByWalletId( ): Promise> { try { const query = ` -SELECT * FROM key_shares +SELECT * FROM "2_key_shares" WHERE wallet_id = $1 LIMIT 1 `; @@ -94,7 +94,7 @@ export async function updateReshare( ): Promise> { try { const query = ` -UPDATE key_shares AS ks +UPDATE "2_key_shares" AS ks SET status = $1, reshared_at = NOW(), diff --git a/key_share_node/pg_interface/src/pg_dumps/index.ts b/key_share_node/pg_interface/src/pg_dumps/index.ts index f8689464c..8192494b3 100644 --- a/key_share_node/pg_interface/src/pg_dumps/index.ts +++ b/key_share_node/pg_interface/src/pg_dumps/index.ts @@ -22,7 +22,7 @@ export interface PgDump { export async function createPgDump(db: Pool): Promise> { try { const query = ` -INSERT INTO pg_dumps ( +INSERT INTO "2_pg_dumps" ( dump_id, status ) VALUES ( $1, $2 @@ -54,7 +54,7 @@ export async function updatePgDump( ): Promise> { try { const query = ` -UPDATE pg_dumps +UPDATE "2_pg_dumps" SET status = $1, dump_path = $2, meta = $3, updated_at = NOW() WHERE dump_id = $4 `; @@ -76,7 +76,7 @@ export async function updatePgDumpStatus( ): Promise> { try { const query = ` -UPDATE pg_dumps +UPDATE "2_pg_dumps" SET status = $1, updated_at = NOW() WHERE dump_id = $2 `; @@ -102,7 +102,7 @@ export async function getOldCompletedPgDumps( const query = ` SELECT * -FROM pg_dumps +FROM "2_pg_dumps" WHERE status = 'COMPLETED' AND created_at < NOW() - ($1 * INTERVAL '1 second') ORDER BY created_at ASC @@ -122,7 +122,7 @@ export async function getPgDumpById( try { const query = ` SELECT * -FROM pg_dumps +FROM "2_pg_dumps" WHERE dump_id = $1 `; const result = await db.query(query, [dumpId]); @@ -141,7 +141,7 @@ export async function getAllPgDumps( ): Promise> { try { let query = ` -SELECT * FROM pg_dumps +SELECT * FROM "2_pg_dumps" `; const values = []; @@ -164,7 +164,7 @@ export async function getLatestCompletedPgDump( ): Promise> { try { const query = ` -SELECT * FROM pg_dumps +SELECT * FROM "2_pg_dumps" WHERE status = 'COMPLETED' ORDER BY created_at DESC LIMIT 1 diff --git a/key_share_node/pg_interface/src/server_keypairs/index.ts b/key_share_node/pg_interface/src/server_keypairs/index.ts index c2575f0b9..17f557e2b 100644 --- a/key_share_node/pg_interface/src/server_keypairs/index.ts +++ b/key_share_node/pg_interface/src/server_keypairs/index.ts @@ -18,7 +18,7 @@ export async function getActiveServerKeypair( ): Promise> { const query = ` SELECT * -FROM server_keypairs +FROM "2_server_keypairs" WHERE is_active = true ORDER BY version DESC LIMIT 1 @@ -50,7 +50,7 @@ export async function getServerKeypairByVersion( ): Promise> { const query = ` SELECT * -FROM server_keypairs +FROM "2_server_keypairs" WHERE version = $1 LIMIT 1 `; @@ -80,7 +80,7 @@ export async function getAllServerKeypairs( ): Promise> { const query = ` SELECT * -FROM server_keypairs +FROM "2_server_keypairs" ORDER BY version DESC `; @@ -107,7 +107,7 @@ export async function insertServerKeypair( }, ): Promise> { const query = ` -INSERT INTO server_keypairs ( +INSERT INTO "2_server_keypairs" ( keypair_id, public_key, enc_private_key, is_active ) VALUES ( @@ -158,7 +158,7 @@ export async function rotateServerKeypair( } await client.query(` -UPDATE server_keypairs +UPDATE "2_server_keypairs" SET is_active = false, rotated_at = now(), updated_at = now() WHERE is_active = true `); @@ -197,7 +197,7 @@ export async function deactivateServerKeypair( keypairId: string, ): Promise> { const query = ` -UPDATE server_keypairs +UPDATE "2_server_keypairs" SET is_active = false, rotated_at = now(), updated_at = now() WHERE keypair_id = $1 `; diff --git a/key_share_node/pg_interface/src/users/index.ts b/key_share_node/pg_interface/src/users/index.ts index 657fbded2..70bf10612 100644 --- a/key_share_node/pg_interface/src/users/index.ts +++ b/key_share_node/pg_interface/src/users/index.ts @@ -9,7 +9,7 @@ export async function createUser( ): Promise> { try { const query = ` -INSERT INTO users ( +INSERT INTO "2_users" ( email, auth_type ) VALUES ( @@ -40,7 +40,7 @@ export async function getUserByEmailAndAuthType( ): Promise> { try { const query = ` -SELECT * FROM users +SELECT * FROM "2_users" WHERE email = $1 AND auth_type = $2 LIMIT 1 `; @@ -63,7 +63,7 @@ export async function getUserFromUserId( ): Promise> { try { const query = ` -SELECT * FROM users +SELECT * FROM "2_users" WHERE user_id = $1 LIMIT 1 `; diff --git a/key_share_node/pg_interface/src/wallets/index.ts b/key_share_node/pg_interface/src/wallets/index.ts index bfb39bfcb..37adc5eae 100644 --- a/key_share_node/pg_interface/src/wallets/index.ts +++ b/key_share_node/pg_interface/src/wallets/index.ts @@ -13,7 +13,7 @@ export async function createWallet( ): Promise> { try { const query = ` -INSERT INTO wallets ( +INSERT INTO "2_wallets" ( wallet_id, user_id, curve_type, public_key ) @@ -59,7 +59,7 @@ export async function getWalletById( ): Promise> { try { const query = ` -SELECT * FROM wallets +SELECT * FROM "2_wallets" WHERE wallet_id = $1 LIMIT 1 `; @@ -89,7 +89,7 @@ export async function getWalletByPublicKey( ): Promise> { try { const query = ` -SELECT * FROM wallets +SELECT * FROM "2_wallets" WHERE public_key = $1 LIMIT 1 `; diff --git a/key_share_node/server/src/routes/pg_dump/index.test.ts b/key_share_node/server/src/routes/pg_dump/index.test.ts index f18405dea..de38ec5c2 100644 --- a/key_share_node/server/src/routes/pg_dump/index.test.ts +++ b/key_share_node/server/src/routes/pg_dump/index.test.ts @@ -3,12 +3,6 @@ import os from "node:os"; import request from "supertest"; import express from "express"; import { Pool } from "pg"; -import fs from "node:fs/promises"; -import { getPgDumpById, getAllPgDumps } from "@oko-wallet/ksn-pg-interface"; -import { - createUser, - getUserByEmailAndAuthType, -} from "@oko-wallet/ksn-pg-interface"; import dayjs from "dayjs"; import { Bytes } from "@oko-wallet/bytes"; @@ -113,188 +107,188 @@ describe("pg_dump_route_test", () => { await resetPgDatabase(pool); }); - describe("POST /pg_dump/v1/backup", () => { - it("should successfully create pg dump with valid password", async () => { - const response = await request(app) - .post("/pg_dump/v1/backup") - .send({ password: testAdminPassword }) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data).toBeDefined(); - expect(response.body.data.dumpId).toBeDefined(); - expect(response.body.data.dumpPath).toBeDefined(); - expect(response.body.data.dumpSize).toBeGreaterThan(0); - expect(response.body.data.dumpDuration).toBeGreaterThanOrEqual(0); - - const dbDump = await getPgDumpById(pool, response.body.data.dumpId); - expect(dbDump.success).toBe(true); - if (dbDump.success === false) { - throw new Error(`getPgDumpById failed: ${dbDump.err}`); - } - expect(dbDump.data?.status).toBe("COMPLETED"); - expect(dbDump.data?.dump_path).toBe(response.body.data.dumpPath); - expect(dbDump.data?.meta.dump_size).toBe(response.body.data.dumpSize); - expect(dbDump.data?.meta.dump_duration).toBe( - response.body.data.dumpDuration, - ); - - const fileExists = await fs - .access(response.body.data.dumpPath) - .then(() => true) - .catch(() => false); - expect(fileExists).toBe(true); - - const stats = await fs.stat(response.body.data.dumpPath); - expect(stats.size).toBe(response.body.data.dumpSize); - - const fileBuffer = await fs.readFile(response.body.data.dumpPath); - expect(fileBuffer.toString("ascii", 0, 5)).toBe("PGDMP"); - }); - - it("should fail with invalid password", async () => { - const response = await request(app) - .post("/pg_dump/v1/backup") - .send({ password: "wrong_password" }) - .expect(401); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("UNAUTHORIZED"); - expect(response.body.msg).toBe("Invalid admin password"); - }); - - it("should fail with missing password", async () => { - const response = await request(app) - .post("/pg_dump/v1/backup") - .send({}) - .expect(401); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("UNAUTHORIZED"); - expect(response.body.msg).toBe("Admin password is required"); - }); - - it("should fail with empty password", async () => { - const response = await request(app) - .post("/pg_dump/v1/backup") - .send({ password: "" }) - .expect(401); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("UNAUTHORIZED"); - expect(response.body.msg).toBe("Admin password is required"); - }); - - it("should handle database configuration errors", async () => { - const originalDbName = process.env.DB_NAME; - process.env.DB_NAME = "non_existent_db"; - - const invalidApp = express(); - invalidApp.use(express.json()); - - const pgDumpRouter = makePgDumpRouter(); - invalidApp.use("/pg_dump/v1", pgDumpRouter); - - invalidApp.locals = makeUnsuccessfulAppStatus(pool); - - const response = await request(invalidApp) - .post("/pg_dump/v1/backup") - .send({ password: testAdminPassword }) - .expect(500); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("PG_DUMP_FAILED"); - expect(response.body.msg).toContain("database"); - - const allDumps = await getAllPgDumps(pool); - expect(allDumps.success).toBe(true); - if (allDumps.success === false) { - throw new Error(`getAllPgDumps failed: ${allDumps.err}`); - } - - const failedDumps = allDumps.data.filter( - (dump) => dump.status === "FAILED", - ); - expect(failedDumps.length).toBe(1); - expect(failedDumps[0].dump_path).toBeNull(); - expect(failedDumps[0].meta.error).toContain("database"); - - process.env.DB_NAME = originalDbName; - }); - - it("should handle authentication errors", async () => { - const invalidApp = express(); - invalidApp.use(express.json()); - - const pgDumpRouter = makePgDumpRouter(); - invalidApp.use("/pg_dump/v1", pgDumpRouter); - - process.env.DB_PASSWORD = "wrong_password"; - - invalidApp.locals = makeUnsuccessfulAppStatus(pool); - - const response = await request(invalidApp) - .post("/pg_dump/v1/backup") - .send({ password: testAdminPassword }) - .expect(500); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("PG_DUMP_FAILED"); - expect(response.body.msg).toContain("authentication"); - - const allDumps = await getAllPgDumps(pool); - expect(allDumps.success).toBe(true); - if (allDumps.success === false) { - throw new Error(`getAllPgDumps failed: ${allDumps.err}`); - } - - const failedDumps = allDumps.data.filter( - (dump) => dump.status === "FAILED", - ); - expect(failedDumps.length).toBe(1); - expect(failedDumps[0].dump_path).toBeNull(); - expect(failedDumps[0].meta.error).toContain("authentication"); - }); - - it("should handle multiple concurrent requests", async () => { - const promises = Array.from({ length: 3 }, () => - request(app) - .post("/pg_dump/v1/backup") - .send({ password: testAdminPassword }) - .expect(200), - ); - - const responses = await Promise.all(promises); - - responses.forEach((response) => { - expect(response.body.success).toBe(true); - expect(response.body.data.dumpId).toBeDefined(); - expect(response.body.data.dumpPath).toBeDefined(); - }); - - const dumpIds = responses.map((r) => r.body.data.dumpId); - const uniqueDumpIds = new Set(dumpIds); - expect(uniqueDumpIds.size).toBe(3); - - for (const response of responses) { - const dbDump = await getPgDumpById(pool, response.body.data.dumpId); - expect(dbDump.success).toBe(true); - if (dbDump.success) { - expect(dbDump.data?.status).toBe("COMPLETED"); - expect(dbDump.data?.dump_path).toBe(response.body.data.dumpPath); - } - - const fileExists = await fs - .access(response.body.data.dumpPath) - .then(() => true) - .catch(() => false); - expect(fileExists).toBe(true); - - const fileBuffer = await fs.readFile(response.body.data.dumpPath); - expect(fileBuffer.toString("ascii", 0, 5)).toBe("PGDMP"); - } - }); - }); + // describe("POST /pg_dump/v1/backup", () => { + // it("should successfully create pg dump with valid password", async () => { + // const response = await request(app) + // .post("/pg_dump/v1/backup") + // .send({ password: testAdminPassword }) + // .expect(200); + + // expect(response.body.success).toBe(true); + // expect(response.body.data).toBeDefined(); + // expect(response.body.data.dumpId).toBeDefined(); + // expect(response.body.data.dumpPath).toBeDefined(); + // expect(response.body.data.dumpSize).toBeGreaterThan(0); + // expect(response.body.data.dumpDuration).toBeGreaterThanOrEqual(0); + + // const dbDump = await getPgDumpById(pool, response.body.data.dumpId); + // expect(dbDump.success).toBe(true); + // if (dbDump.success === false) { + // throw new Error(`getPgDumpById failed: ${dbDump.err}`); + // } + // expect(dbDump.data?.status).toBe("COMPLETED"); + // expect(dbDump.data?.dump_path).toBe(response.body.data.dumpPath); + // expect(dbDump.data?.meta.dump_size).toBe(response.body.data.dumpSize); + // expect(dbDump.data?.meta.dump_duration).toBe( + // response.body.data.dumpDuration, + // ); + + // const fileExists = await fs + // .access(response.body.data.dumpPath) + // .then(() => true) + // .catch(() => false); + // expect(fileExists).toBe(true); + + // const stats = await fs.stat(response.body.data.dumpPath); + // expect(stats.size).toBe(response.body.data.dumpSize); + + // const fileBuffer = await fs.readFile(response.body.data.dumpPath); + // expect(fileBuffer.toString("ascii", 0, 5)).toBe("PGDMP"); + // }); + + // it("should fail with invalid password", async () => { + // const response = await request(app) + // .post("/pg_dump/v1/backup") + // .send({ password: "wrong_password" }) + // .expect(401); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("UNAUTHORIZED"); + // expect(response.body.msg).toBe("Invalid admin password"); + // }); + + // it("should fail with missing password", async () => { + // const response = await request(app) + // .post("/pg_dump/v1/backup") + // .send({}) + // .expect(401); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("UNAUTHORIZED"); + // expect(response.body.msg).toBe("Admin password is required"); + // }); + + // it("should fail with empty password", async () => { + // const response = await request(app) + // .post("/pg_dump/v1/backup") + // .send({ password: "" }) + // .expect(401); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("UNAUTHORIZED"); + // expect(response.body.msg).toBe("Admin password is required"); + // }); + + // it("should handle database configuration errors", async () => { + // const originalDbName = process.env.DB_NAME; + // process.env.DB_NAME = "non_existent_db"; + + // const invalidApp = express(); + // invalidApp.use(express.json()); + + // const pgDumpRouter = makePgDumpRouter(); + // invalidApp.use("/pg_dump/v1", pgDumpRouter); + + // invalidApp.locals = makeUnsuccessfulAppStatus(pool); + + // const response = await request(invalidApp) + // .post("/pg_dump/v1/backup") + // .send({ password: testAdminPassword }) + // .expect(500); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("PG_DUMP_FAILED"); + // expect(response.body.msg).toContain("database"); + + // const allDumps = await getAllPgDumps(pool); + // expect(allDumps.success).toBe(true); + // if (allDumps.success === false) { + // throw new Error(`getAllPgDumps failed: ${allDumps.err}`); + // } + + // const failedDumps = allDumps.data.filter( + // (dump) => dump.status === "FAILED", + // ); + // expect(failedDumps.length).toBe(1); + // expect(failedDumps[0].dump_path).toBeNull(); + // expect(failedDumps[0].meta.error).toContain("database"); + + // process.env.DB_NAME = originalDbName; + // }); + + // it("should handle authentication errors", async () => { + // const invalidApp = express(); + // invalidApp.use(express.json()); + + // const pgDumpRouter = makePgDumpRouter(); + // invalidApp.use("/pg_dump/v1", pgDumpRouter); + + // process.env.DB_PASSWORD = "wrong_password"; + + // invalidApp.locals = makeUnsuccessfulAppStatus(pool); + + // const response = await request(invalidApp) + // .post("/pg_dump/v1/backup") + // .send({ password: testAdminPassword }) + // .expect(500); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("PG_DUMP_FAILED"); + // expect(response.body.msg).toContain("authentication"); + + // const allDumps = await getAllPgDumps(pool); + // expect(allDumps.success).toBe(true); + // if (allDumps.success === false) { + // throw new Error(`getAllPgDumps failed: ${allDumps.err}`); + // } + + // const failedDumps = allDumps.data.filter( + // (dump) => dump.status === "FAILED", + // ); + // expect(failedDumps.length).toBe(1); + // expect(failedDumps[0].dump_path).toBeNull(); + // expect(failedDumps[0].meta.error).toContain("authentication"); + // }); + + // it("should handle multiple concurrent requests", async () => { + // const promises = Array.from({ length: 3 }, () => + // request(app) + // .post("/pg_dump/v1/backup") + // .send({ password: testAdminPassword }) + // .expect(200), + // ); + + // const responses = await Promise.all(promises); + + // responses.forEach((response) => { + // expect(response.body.success).toBe(true); + // expect(response.body.data.dumpId).toBeDefined(); + // expect(response.body.data.dumpPath).toBeDefined(); + // }); + + // const dumpIds = responses.map((r) => r.body.data.dumpId); + // const uniqueDumpIds = new Set(dumpIds); + // expect(uniqueDumpIds.size).toBe(3); + + // for (const response of responses) { + // const dbDump = await getPgDumpById(pool, response.body.data.dumpId); + // expect(dbDump.success).toBe(true); + // if (dbDump.success) { + // expect(dbDump.data?.status).toBe("COMPLETED"); + // expect(dbDump.data?.dump_path).toBe(response.body.data.dumpPath); + // } + + // const fileExists = await fs + // .access(response.body.data.dumpPath) + // .then(() => true) + // .catch(() => false); + // expect(fileExists).toBe(true); + + // const fileBuffer = await fs.readFile(response.body.data.dumpPath); + // expect(fileBuffer.toString("ascii", 0, 5)).toBe("PGDMP"); + // } + // }); + // }); describe("POST /pg_dump/v1/get_backup_history", () => { const createDump = async () => { @@ -454,343 +448,343 @@ WHERE dump_id = $1`, }); }); - describe("POST /pg_dump/v1/restore", () => { - const createDump = async () => { - const response = await request(app) - .post("/pg_dump/v1/backup") - .send({ password: testAdminPassword }) - .expect(200); - return response.body.data; - }; - - it("should successfully restore pg dump with valid dump_path and password", async () => { - // Create test users using createUser function - const testEmails = [ - "user1@test.com", - "user2@test.com", - "user3@test.com", - "user4@test.com", - "user5@test.com", - ]; - - const createdUsers = []; - for (const email of testEmails) { - const createUserRes = await createUser(pool, email, "google"); - expect(createUserRes.success).toBe(true); - if (createUserRes.success) { - createdUsers.push(createUserRes.data); - } - } - - for (const email of testEmails) { - const getUserRes = await getUserByEmailAndAuthType( - pool, - email, - "google", - ); - expect(getUserRes.success).toBe(true); - if (getUserRes.success) { - expect(getUserRes.data).not.toBeNull(); - if (getUserRes.data) { - expect(getUserRes.data.email).toBe(email); - } - } - } - - // Create dump - const dump = await createDump(); - - // Reset database to simulate data loss - await resetPgDatabase(pool); - - for (const email of testEmails) { - const getUserRes = await getUserByEmailAndAuthType( - pool, - email, - "google", - ); - expect(getUserRes.success).toBe(true); - if (getUserRes.success) { - expect(getUserRes.data).toBeNull(); - } - } - - // Restore from dump - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: dump.dumpPath, - }) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data).toBeDefined(); - expect(response.body.data.dump_path).toBe(dump.dumpPath); - - // Verify data was restored - for (const email of testEmails) { - const getUserRes = await getUserByEmailAndAuthType( - pool, - email, - "google", - ); - expect(getUserRes.success).toBe(true); - if (getUserRes.success) { - expect(getUserRes.data).not.toBeNull(); - if (getUserRes.data) { - expect(getUserRes.data.email).toBe(email); - } - } - } - }); - - it("should fail with invalid password", async () => { - const dump = await createDump(); - - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: "wrong_password", - dump_path: dump.dumpPath, - }) - .expect(401); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("UNAUTHORIZED"); - expect(response.body.msg).toBe("Invalid admin password"); - }); - - it("should fail with missing password", async () => { - const dump = await createDump(); - - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - dump_path: dump.dumpPath, - }) - .expect(401); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("UNAUTHORIZED"); - expect(response.body.msg).toBe("Admin password is required"); - }); - - it("should fail with empty password", async () => { - const dump = await createDump(); - - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: "", - dump_path: dump.dumpPath, - }) - .expect(401); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("UNAUTHORIZED"); - expect(response.body.msg).toBe("Admin password is required"); - }); - - it("should fail with missing dump_path", async () => { - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - }) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("INVALID_DUMP_PATH"); - expect(response.body.msg).toBe("dump_path parameter is required"); - }); - - it("should fail with null dump_path", async () => { - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: null, - }) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("INVALID_DUMP_PATH"); - expect(response.body.msg).toBe("dump_path parameter is required"); - }); - - it("should fail with empty string dump_path", async () => { - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: "", - }) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("INVALID_DUMP_PATH"); - expect(response.body.msg).toBe("dump_path parameter is required"); - }); - - it("should fail with non-existent dump_path", async () => { - const nonExistentPath = "/path/to/non/existent/dump.dump"; - - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: nonExistentPath, - }) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("DUMP_FILE_NOT_FOUND"); - expect(response.body.msg).toBe( - `Dump file not found at path: ${nonExistentPath}`, - ); - }); - - it("should fail when dump_path points to a directory", async () => { - const directoryPath = "/tmp"; - - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: directoryPath, - }) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("INVALID_DUMP_FILE"); - expect(response.body.msg).toBe(`Path is not a file: ${directoryPath}`); - }); - - it("should handle database configuration errors during restore", async () => { - const dump = await createDump(); - - const invalidApp = express(); - invalidApp.use(express.json()); - - const pgDumpRouter = makePgDumpRouter(); - invalidApp.use("/pg_dump/v1", pgDumpRouter); - - process.env.DB_NAME = "non_existent_db"; - - invalidApp.locals = makeUnsuccessfulAppStatus(pool); - - const response = await request(invalidApp) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: dump.dumpPath, - }) - .expect(500); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("PG_RESTORE_FAILED"); - expect(response.body.msg).toContain("database"); - }); - - it("should handle authentication errors during restore", async () => { - const dump = await createDump(); - - const invalidApp = express(); - invalidApp.use(express.json()); - - const pgDumpRouter = makePgDumpRouter(); - invalidApp.use("/pg_dump/v1", pgDumpRouter); - - process.env.DB_PASSWORD = "wrong_password"; - - invalidApp.locals = makeUnsuccessfulAppStatus(pool); - - const response = await request(invalidApp) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: dump.dumpPath, - }) - .expect(500); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("PG_RESTORE_FAILED"); - expect(response.body.msg).toContain("authentication"); - }); - - it("should handle non-existent dump file", async () => { - const dump = await createDump(); - - // Delete the dump file to simulate a missing file - await fs.unlink(dump.dumpPath); - - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: dump.dumpPath, - }) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("DUMP_FILE_NOT_FOUND"); - expect(response.body.msg).toBe( - `Dump file not found at path: ${dump.dumpPath}`, - ); - }); - - it("should handle corrupted dump file", async () => { - const dump = await createDump(); - - // Corrupt the dump file by writing invalid data - await fs.writeFile(dump.dumpPath, "invalid dump data"); - - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: dump.dumpPath, - }) - .expect(500); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe("PG_RESTORE_FAILED"); - }); - - it("should handle relative dump_path", async () => { - const dump = await createDump(); - - // Get relative path from absolute path - const relativePath = dump.dumpPath.replace(process.cwd(), "."); - - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: relativePath, - }) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data).toBeDefined(); - expect(response.body.data.dump_path).toBe(relativePath); - }); - - it("should handle absolute dump_path", async () => { - const dump = await createDump(); - - const response = await request(app) - .post("/pg_dump/v1/restore") - .send({ - password: testAdminPassword, - dump_path: dump.dumpPath, - }) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data).toBeDefined(); - expect(response.body.data.dump_path).toBe(dump.dumpPath); - }); - }); + // describe("POST /pg_dump/v1/restore", () => { + // const createDump = async () => { + // const response = await request(app) + // .post("/pg_dump/v1/backup") + // .send({ password: testAdminPassword }) + // .expect(200); + // return response.body.data; + // }; + + // it("should successfully restore pg dump with valid dump_path and password", async () => { + // // Create test users using createUser function + // const testEmails = [ + // "user1@test.com", + // "user2@test.com", + // "user3@test.com", + // "user4@test.com", + // "user5@test.com", + // ]; + + // const createdUsers = []; + // for (const email of testEmails) { + // const createUserRes = await createUser(pool, email, "google"); + // expect(createUserRes.success).toBe(true); + // if (createUserRes.success) { + // createdUsers.push(createUserRes.data); + // } + // } + + // for (const email of testEmails) { + // const getUserRes = await getUserByEmailAndAuthType( + // pool, + // email, + // "google", + // ); + // expect(getUserRes.success).toBe(true); + // if (getUserRes.success) { + // expect(getUserRes.data).not.toBeNull(); + // if (getUserRes.data) { + // expect(getUserRes.data.email).toBe(email); + // } + // } + // } + + // // Create dump + // const dump = await createDump(); + + // // Reset database to simulate data loss + // await resetPgDatabase(pool); + + // for (const email of testEmails) { + // const getUserRes = await getUserByEmailAndAuthType( + // pool, + // email, + // "google", + // ); + // expect(getUserRes.success).toBe(true); + // if (getUserRes.success) { + // expect(getUserRes.data).toBeNull(); + // } + // } + + // // Restore from dump + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: dump.dumpPath, + // }) + // .expect(200); + + // expect(response.body.success).toBe(true); + // expect(response.body.data).toBeDefined(); + // expect(response.body.data.dump_path).toBe(dump.dumpPath); + + // // Verify data was restored + // for (const email of testEmails) { + // const getUserRes = await getUserByEmailAndAuthType( + // pool, + // email, + // "google", + // ); + // expect(getUserRes.success).toBe(true); + // if (getUserRes.success) { + // expect(getUserRes.data).not.toBeNull(); + // if (getUserRes.data) { + // expect(getUserRes.data.email).toBe(email); + // } + // } + // } + // }); + + // it("should fail with invalid password", async () => { + // const dump = await createDump(); + + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: "wrong_password", + // dump_path: dump.dumpPath, + // }) + // .expect(401); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("UNAUTHORIZED"); + // expect(response.body.msg).toBe("Invalid admin password"); + // }); + + // it("should fail with missing password", async () => { + // const dump = await createDump(); + + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // dump_path: dump.dumpPath, + // }) + // .expect(401); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("UNAUTHORIZED"); + // expect(response.body.msg).toBe("Admin password is required"); + // }); + + // it("should fail with empty password", async () => { + // const dump = await createDump(); + + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: "", + // dump_path: dump.dumpPath, + // }) + // .expect(401); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("UNAUTHORIZED"); + // expect(response.body.msg).toBe("Admin password is required"); + // }); + + // it("should fail with missing dump_path", async () => { + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // }) + // .expect(400); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("INVALID_DUMP_PATH"); + // expect(response.body.msg).toBe("dump_path parameter is required"); + // }); + + // it("should fail with null dump_path", async () => { + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: null, + // }) + // .expect(400); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("INVALID_DUMP_PATH"); + // expect(response.body.msg).toBe("dump_path parameter is required"); + // }); + + // it("should fail with empty string dump_path", async () => { + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: "", + // }) + // .expect(400); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("INVALID_DUMP_PATH"); + // expect(response.body.msg).toBe("dump_path parameter is required"); + // }); + + // it("should fail with non-existent dump_path", async () => { + // const nonExistentPath = "/path/to/non/existent/dump.dump"; + + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: nonExistentPath, + // }) + // .expect(400); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("DUMP_FILE_NOT_FOUND"); + // expect(response.body.msg).toBe( + // `Dump file not found at path: ${nonExistentPath}`, + // ); + // }); + + // it("should fail when dump_path points to a directory", async () => { + // const directoryPath = "/tmp"; + + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: directoryPath, + // }) + // .expect(400); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("INVALID_DUMP_FILE"); + // expect(response.body.msg).toBe(`Path is not a file: ${directoryPath}`); + // }); + + // it("should handle database configuration errors during restore", async () => { + // const dump = await createDump(); + + // const invalidApp = express(); + // invalidApp.use(express.json()); + + // const pgDumpRouter = makePgDumpRouter(); + // invalidApp.use("/pg_dump/v1", pgDumpRouter); + + // process.env.DB_NAME = "non_existent_db"; + + // invalidApp.locals = makeUnsuccessfulAppStatus(pool); + + // const response = await request(invalidApp) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: dump.dumpPath, + // }) + // .expect(500); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("PG_RESTORE_FAILED"); + // expect(response.body.msg).toContain("database"); + // }); + + // it("should handle authentication errors during restore", async () => { + // const dump = await createDump(); + + // const invalidApp = express(); + // invalidApp.use(express.json()); + + // const pgDumpRouter = makePgDumpRouter(); + // invalidApp.use("/pg_dump/v1", pgDumpRouter); + + // process.env.DB_PASSWORD = "wrong_password"; + + // invalidApp.locals = makeUnsuccessfulAppStatus(pool); + + // const response = await request(invalidApp) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: dump.dumpPath, + // }) + // .expect(500); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("PG_RESTORE_FAILED"); + // expect(response.body.msg).toContain("authentication"); + // }); + + // it("should handle non-existent dump file", async () => { + // const dump = await createDump(); + + // // Delete the dump file to simulate a missing file + // await fs.unlink(dump.dumpPath); + + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: dump.dumpPath, + // }) + // .expect(400); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("DUMP_FILE_NOT_FOUND"); + // expect(response.body.msg).toBe( + // `Dump file not found at path: ${dump.dumpPath}`, + // ); + // }); + + // it("should handle corrupted dump file", async () => { + // const dump = await createDump(); + + // // Corrupt the dump file by writing invalid data + // await fs.writeFile(dump.dumpPath, "invalid dump data"); + + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: dump.dumpPath, + // }) + // .expect(500); + + // expect(response.body.success).toBe(false); + // expect(response.body.code).toBe("PG_RESTORE_FAILED"); + // }); + + // it("should handle relative dump_path", async () => { + // const dump = await createDump(); + + // // Get relative path from absolute path + // const relativePath = dump.dumpPath.replace(process.cwd(), "."); + + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: relativePath, + // }) + // .expect(200); + + // expect(response.body.success).toBe(true); + // expect(response.body.data).toBeDefined(); + // expect(response.body.data.dump_path).toBe(relativePath); + // }); + + // it("should handle absolute dump_path", async () => { + // const dump = await createDump(); + + // const response = await request(app) + // .post("/pg_dump/v1/restore") + // .send({ + // password: testAdminPassword, + // dump_path: dump.dumpPath, + // }) + // .expect(200); + + // expect(response.body.success).toBe(true); + // expect(response.body.data).toBeDefined(); + // expect(response.body.data.dump_path).toBe(dump.dumpPath); + // }); + // }); }); diff --git a/key_share_node/server/src/routes/pg_dump/index.ts b/key_share_node/server/src/routes/pg_dump/index.ts index cd784b628..477d10be4 100644 --- a/key_share_node/server/src/routes/pg_dump/index.ts +++ b/key_share_node/server/src/routes/pg_dump/index.ts @@ -1,39 +1,20 @@ import { Router, type Response } from "express"; -import fs from "node:fs/promises"; import type { KSNodeApiErrorResponse, KSNodeApiResponse, } from "@oko-wallet/ksn-interface/response"; -import { - getAllPgDumps, - restore, - type PgDump, -} from "@oko-wallet/ksn-pg-interface"; -import type { - GetBackupHistoryRequest, - DBRestoreRequest, - DBRestoreResponse, -} from "@oko-wallet/ksn-interface/db_backup"; +import { getAllPgDumps, type PgDump } from "@oko-wallet/ksn-pg-interface"; +import type { GetBackupHistoryRequest } from "@oko-wallet/ksn-interface/db_backup"; import { - processPgDump, - type PgDumpResult, -} from "@oko-wallet-ksn-server/pg_dump/dump"; -import { - adminAuthMiddleware, rateLimitMiddleware, type AdminAuthenticatedRequest, type RateLimitMiddlewareOption, } from "@oko-wallet-ksn-server/middlewares"; import { ErrorCodeMap } from "@oko-wallet-ksn-server/error"; -import type { KSNodeRequest } from "@oko-wallet-ksn-server/routes/io"; import { registry } from "@oko-wallet-ksn-server/openapi/registry"; import { - PgDumpRequestBodySchema, - PgRestoreRequestBodySchema, - PgDumpSuccessResponseSchema, PgDumpHistorySuccessResponseSchema, - PgRestoreSuccessResponseSchema, PgDumpHistoryQuerySchema, ErrorResponseSchema, } from "@oko-wallet-ksn-server/openapi/schema"; @@ -45,14 +26,90 @@ const ADMIN_RATE_LIMIT: RateLimitMiddlewareOption = { maxRequests: 5, }; -const RESTORE_RATE_LIMIT: RateLimitMiddlewareOption = { - windowSeconds: 60, - maxRequests: 3, -}; - export function makePgDumpRouter() { const router = Router(); + registry.registerPath({ + method: "post", + path: "/pg_dump/v1/get_backup_history", + tags: ["PG Dump"], + summary: "Get pg dump history", + description: "Get pg dump history for the specified number of days.", + request: { + query: PgDumpHistoryQuerySchema, + }, + responses: { + 200: { + description: "Successfully retrieved pg dump history", + content: { + "application/json": { + schema: PgDumpHistorySuccessResponseSchema, + }, + }, + }, + 400: { + description: "Invalid days parameter", + content: { + "application/json": { + schema: ErrorResponseSchema, + examples: { + INVALID_DAYS: { + value: { + success: false, + code: "INVALID_DAYS", + msg: "Days parameter must be between 1 and 1000", + }, + }, + }, + }, + }, + }, + 500: { + description: "Failed to retrieve pg dump history", + content: { + "application/json": { + schema: ErrorResponseSchema, + examples: { + UNKNOWN_ERROR: { + value: { + success: false, + code: "UNKNOWN_ERROR", + msg: "Failed to retrieve pg dump history", + }, + }, + }, + }, + }, + }, + }, + }); + router.post( + "/get_backup_history", + ...(isTest ? [] : [rateLimitMiddleware(ADMIN_RATE_LIMIT)]), + async ( + req: AdminAuthenticatedRequest, + res: Response>, + ) => { + const { days } = req.body; + const state = req.app.locals; + + const dumpsResult = await getAllPgDumps(state.db, days); + if (dumpsResult.success === false) { + const errorRes: KSNodeApiErrorResponse = { + success: false, + code: "UNKNOWN_ERROR", + msg: dumpsResult.err, + }; + return res.status(ErrorCodeMap[errorRes.code]).json(errorRes); + } + + return res.status(200).json({ + success: true, + data: dumpsResult.data, + }); + }, + ); + // registry.registerPath({ // method: "post", // path: "/pg_dump/v1/backup", @@ -151,88 +208,6 @@ export function makePgDumpRouter() { // }, // ); - registry.registerPath({ - method: "post", - path: "/pg_dump/v1/get_backup_history", - tags: ["PG Dump"], - summary: "Get pg dump history", - description: "Get pg dump history for the specified number of days.", - request: { - query: PgDumpHistoryQuerySchema, - }, - responses: { - 200: { - description: "Successfully retrieved pg dump history", - content: { - "application/json": { - schema: PgDumpHistorySuccessResponseSchema, - }, - }, - }, - 400: { - description: "Invalid days parameter", - content: { - "application/json": { - schema: ErrorResponseSchema, - examples: { - INVALID_DAYS: { - value: { - success: false, - code: "INVALID_DAYS", - msg: "Days parameter must be between 1 and 1000", - }, - }, - }, - }, - }, - }, - 500: { - description: "Failed to retrieve pg dump history", - content: { - "application/json": { - schema: ErrorResponseSchema, - examples: { - UNKNOWN_ERROR: { - value: { - success: false, - code: "UNKNOWN_ERROR", - msg: "Failed to retrieve pg dump history", - }, - }, - }, - }, - }, - }, - }, - }); - router.post( - "/get_backup_history", - ...(isTest ? [] : [rateLimitMiddleware(ADMIN_RATE_LIMIT)]), - adminAuthMiddleware, - async ( - req: AdminAuthenticatedRequest, - res: Response>, - ) => { - const { days } = req.body; - const state = req.app.locals; - - const dumpsResult = await getAllPgDumps(state.db, days); - if (dumpsResult.success === false) { - const errorRes: KSNodeApiErrorResponse = { - success: false, - code: "UNKNOWN_ERROR", - msg: dumpsResult.err, - }; - return res.status(ErrorCodeMap[errorRes.code]).json(errorRes); - } - - return res.status(200).json({ - success: true, - data: dumpsResult.data, - }); - }, - ); - // registry.registerPath({ // method: "post", // path: "/pg_dump/v1/restore",