diff --git a/docs/modules/postgresql.md b/docs/modules/postgresql.md index ebd8ae14a..4bcb903fc 100644 --- a/docs/modules/postgresql.md +++ b/docs/modules/postgresql.md @@ -38,5 +38,5 @@ tests very modular, since they always run on a brand-new database. not work if the database for the container is set to `"postgres"`. -[Test with a reusable Postgres container](../../packages/modules/postgresql/src/postgresql-container.test.ts) inside_block:createAndRestoreFromSnapshot +[Test with a reusable Postgres container](../../packages/modules/postgresql/src/postgresql-container-snapshot.test.ts) inside_block:createAndRestoreFromSnapshot \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cbeae833c..0a2fc4417 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@eslint/js": "^9.22.0", "@eslint/json": "^0.11.0", "@vitest/coverage-v8": "^3.1.1", - "cross-env": "^7.0.3", "eslint": "^9.22.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.3", @@ -8022,24 +8021,6 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, - "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" - } - }, "node_modules/cross-fetch": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", diff --git a/package.json b/package.json index 41af06021..9e4d5293e 100755 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "pre-commit": "lint-staged", "docs:serve": "docker-compose up", - "test": "cross-env NODE_ENV=test DEBUG=testcontainers* vitest run", + "test": "vitest run", "test:ci": "npm run test -- --coverage", "format": "prettier --write package.json \"packages/**/*.ts\"", "lint": "eslint --fix package.json \"packages/**/*.ts\"", @@ -21,7 +21,6 @@ "@eslint/js": "^9.22.0", "@eslint/json": "^0.11.0", "@vitest/coverage-v8": "^3.1.1", - "cross-env": "^7.0.3", "eslint": "^9.22.0", "eslint-config-prettier": "^10.1.1", "eslint-plugin-prettier": "^5.2.3", diff --git a/packages/modules/postgresql/src/pgvector-container.test.ts b/packages/modules/postgresql/src/pgvector-container.test.ts new file mode 100644 index 000000000..c7c972961 --- /dev/null +++ b/packages/modules/postgresql/src/pgvector-container.test.ts @@ -0,0 +1,45 @@ +import { Client } from "pg"; +import { PostgreSqlContainer } from "./postgresql-container"; + +const IMAGE = "pgvector/pgvector:pg16"; + +describe("PgvectorContainer", { timeout: 180_000 }, () => { + it("should work", async () => { + const container = await new PostgreSqlContainer(IMAGE).start(); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getPassword(), + }); + await client.connect(); + + const result = await client.query("SELECT 1"); + expect(result.rows[0]).toEqual({ "?column?": 1 }); + + await client.end(); + await container.stop(); + }); + + it("should restart", async () => { + const container = await new PostgreSqlContainer(IMAGE).start(); + await container.restart(); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getPassword(), + }); + await client.connect(); + + const result = await client.query("SELECT 1"); + expect(result.rows[0]).toEqual({ "?column?": 1 }); + + await client.end(); + await container.stop(); + }); +}); diff --git a/packages/modules/postgresql/src/postgis-container.test.ts b/packages/modules/postgresql/src/postgis-container.test.ts new file mode 100644 index 000000000..db86cbcac --- /dev/null +++ b/packages/modules/postgresql/src/postgis-container.test.ts @@ -0,0 +1,45 @@ +import { Client } from "pg"; +import { PostgreSqlContainer } from "./postgresql-container"; + +const IMAGE = "postgis/postgis:16-3.4"; + +describe("PostgisContainer", { timeout: 180_000 }, () => { + it("should work", async () => { + const container = await new PostgreSqlContainer(IMAGE).start(); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getPassword(), + }); + await client.connect(); + + const result = await client.query("SELECT 1"); + expect(result.rows[0]).toEqual({ "?column?": 1 }); + + await client.end(); + await container.stop(); + }); + + it("should restart", async () => { + const container = await new PostgreSqlContainer(IMAGE).start(); + await container.restart(); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getPassword(), + }); + await client.connect(); + + const result = await client.query("SELECT 1"); + expect(result.rows[0]).toEqual({ "?column?": 1 }); + + await client.end(); + await container.stop(); + }); +}); diff --git a/packages/modules/postgresql/src/postgresql-container-snapshot.test.ts b/packages/modules/postgresql/src/postgresql-container-snapshot.test.ts new file mode 100644 index 000000000..9d1114c79 --- /dev/null +++ b/packages/modules/postgresql/src/postgresql-container-snapshot.test.ts @@ -0,0 +1,208 @@ +import { Client } from "pg"; +import { PostgreSqlContainer } from "./postgresql-container"; + +describe("PostgreSqlContainer snapshot and restore", { timeout: 180_000 }, () => { + // createAndRestoreFromSnapshot { + it("should create and restore from snapshot", async () => { + const container = await new PostgreSqlContainer().start(); + + // Connect to the database + let client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Create some test data + await client.query("CREATE TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)"); + await client.query("INSERT INTO test_table (name) VALUES ('initial data')"); + + // Close connection before snapshot (otherwise we'll get an error because user is already connected) + await client.end(); + + // Take a snapshot + await container.snapshot(); + + // Reconnect to database + client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Modify the database + await client.query("INSERT INTO test_table (name) VALUES ('data after snapshot')"); + + // Verify both records exist + let result = await client.query("SELECT * FROM test_table ORDER BY id"); + expect(result.rows).toHaveLength(2); + expect(result.rows[0].name).toEqual("initial data"); + expect(result.rows[1].name).toEqual("data after snapshot"); + + // Close connection before restore (same reason as above) + await client.end(); + + // Restore to the snapshot + await container.restoreSnapshot(); + + // Reconnect to database + client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Verify only the initial data exists after restore + result = await client.query("SELECT * FROM test_table ORDER BY id"); + expect(result.rows).toHaveLength(1); + expect(result.rows[0].name).toEqual("initial data"); + + await client.end(); + await container.stop(); + }); + // } + + it("should use custom snapshot name", async () => { + const container = await new PostgreSqlContainer().start(); + const customSnapshotName = "my_custom_snapshot"; + + // Connect to the database + let client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Create a test table and insert data + await client.query("CREATE TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)"); + await client.query("INSERT INTO test_table (name) VALUES ('initial data')"); + + // Close connection before snapshot + await client.end(); + + // Take a snapshot with custom name + await container.snapshot(customSnapshotName); + + // Reconnect to database + client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Modify the database + await client.query("INSERT INTO test_table (name) VALUES ('data after snapshot')"); + + // Close connection before restore + await client.end(); + + // Restore using the custom snapshot name + await container.restoreSnapshot(customSnapshotName); + + // Reconnect to database + client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Verify only the initial data exists after restore + const result = await client.query("SELECT * FROM test_table ORDER BY id"); + expect(result.rows).toHaveLength(1); + expect(result.rows[0].name).toEqual("initial data"); + + await client.end(); + await container.stop(); + }); + + it("should handle multiple snapshots", async () => { + const container = await new PostgreSqlContainer().start(); + + // Connect to the database + let client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Create a test table + await client.query("CREATE TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)"); + + // Close connection before snapshot + await client.end(); + + // Take first snapshot with empty table + await container.snapshot("snapshot1"); + + // Reconnect to database + client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Add first record + await client.query("INSERT INTO test_table (name) VALUES ('data for snapshot 2')"); + + // Close connection before snapshot + await client.end(); + + // Take second snapshot with one record + await container.snapshot("snapshot2"); + + // Reconnect to database + client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Add second record + await client.query("INSERT INTO test_table (name) VALUES ('data after snapshots')"); + + // Verify we have two records + let result = await client.query("SELECT COUNT(*) as count FROM test_table"); + expect(result.rows[0].count).toEqual("2"); + + // Close connection before restore + await client.end(); + + // Restore to first snapshot (empty table) + await container.restoreSnapshot("snapshot1"); + + // Reconnect to database + client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Verify table is empty + result = await client.query("SELECT COUNT(*) as count FROM test_table"); + expect(result.rows[0].count).toEqual("0"); + + // Close connection before restore + await client.end(); + + // Restore to second snapshot (one record) + await container.restoreSnapshot("snapshot2"); + + // Reconnect to database + client = new Client({ + connectionString: container.getConnectionUri(), + }); + await client.connect(); + + // Verify we have one record + result = await client.query("SELECT * FROM test_table"); + expect(result.rows).toHaveLength(1); + expect(result.rows[0].name).toEqual("data for snapshot 2"); + + await client.end(); + await container.stop(); + }); + + it("should throw an error when trying to snapshot postgres system database", async () => { + const container = await new PostgreSqlContainer().withDatabase("postgres").start(); + + await expect(container.snapshot()).rejects.toThrow( + "Snapshot feature is not supported when using the postgres system database" + ); + + await expect(container.restoreSnapshot()).rejects.toThrow( + "Snapshot feature is not supported when using the postgres system database" + ); + + await container.stop(); + }); +}); diff --git a/packages/modules/postgresql/src/postgresql-container.test.ts b/packages/modules/postgresql/src/postgresql-container.test.ts index 52ff5ede9..9dbffaf02 100644 --- a/packages/modules/postgresql/src/postgresql-container.test.ts +++ b/packages/modules/postgresql/src/postgresql-container.test.ts @@ -113,209 +113,3 @@ describe("PostgreSqlContainer", { timeout: 180_000 }, () => { await expect(() => container.start()).rejects.toThrow(); }); }); - -describe("PostgreSqlContainer snapshot and restore", { timeout: 180_000 }, () => { - // createAndRestoreFromSnapshot { - it("should create and restore from snapshot", async () => { - const container = await new PostgreSqlContainer().start(); - - // Connect to the database - let client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Create some test data - await client.query("CREATE TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)"); - await client.query("INSERT INTO test_table (name) VALUES ('initial data')"); - - // Close connection before snapshot (otherwise we'll get an error because user is already connected) - await client.end(); - - // Take a snapshot - await container.snapshot(); - - // Reconnect to database - client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Modify the database - await client.query("INSERT INTO test_table (name) VALUES ('data after snapshot')"); - - // Verify both records exist - let result = await client.query("SELECT * FROM test_table ORDER BY id"); - expect(result.rows).toHaveLength(2); - expect(result.rows[0].name).toEqual("initial data"); - expect(result.rows[1].name).toEqual("data after snapshot"); - - // Close connection before restore (same reason as above) - await client.end(); - - // Restore to the snapshot - await container.restoreSnapshot(); - - // Reconnect to database - client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Verify only the initial data exists after restore - result = await client.query("SELECT * FROM test_table ORDER BY id"); - expect(result.rows).toHaveLength(1); - expect(result.rows[0].name).toEqual("initial data"); - - await client.end(); - await container.stop(); - }); - // } - - it("should use custom snapshot name", async () => { - const container = await new PostgreSqlContainer().start(); - const customSnapshotName = "my_custom_snapshot"; - - // Connect to the database - let client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Create a test table and insert data - await client.query("CREATE TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)"); - await client.query("INSERT INTO test_table (name) VALUES ('initial data')"); - - // Close connection before snapshot - await client.end(); - - // Take a snapshot with custom name - await container.snapshot(customSnapshotName); - - // Reconnect to database - client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Modify the database - await client.query("INSERT INTO test_table (name) VALUES ('data after snapshot')"); - - // Close connection before restore - await client.end(); - - // Restore using the custom snapshot name - await container.restoreSnapshot(customSnapshotName); - - // Reconnect to database - client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Verify only the initial data exists after restore - const result = await client.query("SELECT * FROM test_table ORDER BY id"); - expect(result.rows).toHaveLength(1); - expect(result.rows[0].name).toEqual("initial data"); - - await client.end(); - await container.stop(); - }); - - it("should handle multiple snapshots", async () => { - const container = await new PostgreSqlContainer().start(); - - // Connect to the database - let client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Create a test table - await client.query("CREATE TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)"); - - // Close connection before snapshot - await client.end(); - - // Take first snapshot with empty table - await container.snapshot("snapshot1"); - - // Reconnect to database - client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Add first record - await client.query("INSERT INTO test_table (name) VALUES ('data for snapshot 2')"); - - // Close connection before snapshot - await client.end(); - - // Take second snapshot with one record - await container.snapshot("snapshot2"); - - // Reconnect to database - client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Add second record - await client.query("INSERT INTO test_table (name) VALUES ('data after snapshots')"); - - // Verify we have two records - let result = await client.query("SELECT COUNT(*) as count FROM test_table"); - expect(result.rows[0].count).toEqual("2"); - - // Close connection before restore - await client.end(); - - // Restore to first snapshot (empty table) - await container.restoreSnapshot("snapshot1"); - - // Reconnect to database - client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Verify table is empty - result = await client.query("SELECT COUNT(*) as count FROM test_table"); - expect(result.rows[0].count).toEqual("0"); - - // Close connection before restore - await client.end(); - - // Restore to second snapshot (one record) - await container.restoreSnapshot("snapshot2"); - - // Reconnect to database - client = new Client({ - connectionString: container.getConnectionUri(), - }); - await client.connect(); - - // Verify we have one record - result = await client.query("SELECT * FROM test_table"); - expect(result.rows).toHaveLength(1); - expect(result.rows[0].name).toEqual("data for snapshot 2"); - - await client.end(); - await container.stop(); - }); - - it("should throw an error when trying to snapshot postgres system database", async () => { - const container = await new PostgreSqlContainer().withDatabase("postgres").start(); - - await expect(container.snapshot()).rejects.toThrow( - "Snapshot feature is not supported when using the postgres system database" - ); - - await expect(container.restoreSnapshot()).rejects.toThrow( - "Snapshot feature is not supported when using the postgres system database" - ); - - await container.stop(); - }); -}); diff --git a/packages/modules/postgresql/src/postgresql-container.ts b/packages/modules/postgresql/src/postgresql-container.ts index b841b672d..9bd774008 100755 --- a/packages/modules/postgresql/src/postgresql-container.ts +++ b/packages/modules/postgresql/src/postgresql-container.ts @@ -37,7 +37,10 @@ export class PostgreSqlContainer extends GenericContainer { }); if (!this.healthCheck) { this.withHealthCheck({ - test: ["CMD-SHELL", `PGPASSWORD=${this.password} psql -U ${this.username} -d ${this.database} -c 'SELECT 1;'`], + test: [ + "CMD-SHELL", + `PGPASSWORD=${this.password} pg_isready --host localhost --username ${this.username} --dbname ${this.database}`, + ], interval: 250, timeout: 1000, retries: 1000, diff --git a/packages/modules/postgresql/src/timescale-container.test.ts b/packages/modules/postgresql/src/timescale-container.test.ts new file mode 100644 index 000000000..c66a4ba15 --- /dev/null +++ b/packages/modules/postgresql/src/timescale-container.test.ts @@ -0,0 +1,45 @@ +import { Client } from "pg"; +import { PostgreSqlContainer } from "./postgresql-container"; + +const IMAGE = "timescale/timescaledb:2.1.0-pg11"; + +describe("TimescaleContainer", { timeout: 180_000 }, () => { + it("should work", async () => { + const container = await new PostgreSqlContainer(IMAGE).start(); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getPassword(), + }); + await client.connect(); + + const result = await client.query("SELECT 1"); + expect(result.rows[0]).toEqual({ "?column?": 1 }); + + await client.end(); + await container.stop(); + }); + + it("should restart", async () => { + const container = await new PostgreSqlContainer(IMAGE).start(); + await container.restart(); + + const client = new Client({ + host: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + user: container.getUsername(), + password: container.getPassword(), + }); + await client.connect(); + + const result = await client.query("SELECT 1"); + expect(result.rows[0]).toEqual({ "?column?": 1 }); + + await client.end(); + await container.stop(); + }); +}); diff --git a/packages/testcontainers/src/generic-container/generic-container.ts b/packages/testcontainers/src/generic-container/generic-container.ts index 5b50bc91c..6aad8e9b4 100644 --- a/packages/testcontainers/src/generic-container/generic-container.ts +++ b/packages/testcontainers/src/generic-container/generic-container.ts @@ -402,7 +402,7 @@ export class GenericContainer implements TestContainer { Test: healthCheck.test, Interval: healthCheck.interval ? toNanos(healthCheck.interval) : 0, Timeout: healthCheck.timeout ? toNanos(healthCheck.timeout) : 0, - Retries: healthCheck.retries || 0, + Retries: healthCheck.retries ?? 0, StartPeriod: healthCheck.startPeriod ? toNanos(healthCheck.startPeriod) : 0, }; diff --git a/packages/testcontainers/src/generic-container/started-generic-container.ts b/packages/testcontainers/src/generic-container/started-generic-container.ts index e97d8aae6..7bb052d76 100644 --- a/packages/testcontainers/src/generic-container/started-generic-container.ts +++ b/packages/testcontainers/src/generic-container/started-generic-container.ts @@ -16,7 +16,7 @@ import { StoppedGenericContainer } from "./stopped-generic-container"; export class StartedGenericContainer implements StartedTestContainer { private stoppedContainer?: StoppedTestContainer; - private stopContainerLock = new AsyncLock(); + private readonly stopContainerLock = new AsyncLock(); constructor( private readonly container: Dockerode.Container, diff --git a/packages/testcontainers/src/test-container.ts b/packages/testcontainers/src/test-container.ts index f4154ca78..148ef18e6 100644 --- a/packages/testcontainers/src/test-container.ts +++ b/packages/testcontainers/src/test-container.ts @@ -47,7 +47,6 @@ export interface TestContainer { withCopyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): this; withCopyContentToContainer(contentsToCopy: ContentToCopy[]): this; withCopyArchivesToContainer(archivesToCopy: ArchiveToCopy[]): this; - withWorkingDir(workingDir: string): this; withResourcesQuota(resourcesQuota: ResourcesQuota): this; withSharedMemorySize(bytes: number): this; diff --git a/vitest.config.ts b/vitest.config.ts index fb3d8d26d..77ca20caa 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,12 @@ -import path from "path"; +import * as path from "path"; import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, + env: { + DEBUG: "testcontainers*", + }, silent: "passed-only", mockReset: true, restoreMocks: true,