diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index ee967d346..b46360668 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -96,6 +96,30 @@ jobs: env: {} - package: graphile/graphile-sql-expression-validator env: {} + - package: graphql/server-test + env: {} + - package: graphql/env + env: {} + - package: graphql/server + env: {} + - package: graphql/test + env: {} + - package: graphql/playwright-test + env: {} + # - package: jobs/knative-job-worker + # env: {} + - package: packages/csv-to-pg + env: {} + - package: packages/smtppostmaster + env: {} + - package: postgres/drizzle-orm-test + env: {} + - package: uploads/etag-hash + env: {} + - package: uploads/etag-stream + env: {} + - package: uploads/stream-to-etag + env: {} env: PGHOST: localhost diff --git a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap index 2f32db1c3..02c79c085 100644 --- a/graphql/env/__tests__/__snapshots__/merge.test.ts.snap +++ b/graphql/env/__tests__/__snapshots__/merge.test.ts.snap @@ -120,5 +120,12 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and "strictAuth": false, "trustProxy": false, }, + "smtp": { + "debug": false, + "logger": false, + "pool": false, + "port": 587, + "secure": false, + }, } `; diff --git a/graphql/server-test/__tests__/__snapshots__/server-test.test.ts.snap b/graphql/server-test/__tests__/__snapshots__/server-test.test.ts.snap new file mode 100644 index 000000000..8e3c073a8 --- /dev/null +++ b/graphql/server-test/__tests__/__snapshots__/server-test.test.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`graphql-server-test getConnections should snapshot query results 1`] = ` +{ + "users": { + "nodes": [ + { + "email": "alice@example.com", + "username": "alice", + }, + { + "email": "bob@example.com", + "username": "bob", + }, + ], + }, +} +`; diff --git a/graphql/server-test/__tests__/server-test.test.ts b/graphql/server-test/__tests__/server-test.test.ts index cd4ddd293..30e4ad6f1 100644 --- a/graphql/server-test/__tests__/server-test.test.ts +++ b/graphql/server-test/__tests__/server-test.test.ts @@ -39,28 +39,29 @@ describe('graphql-server-test', () => { afterEach(() => db.afterEach()); it('should have a running server', () => { - expect(server.url).toMatch(/^http:\/\/localhost:\d+$/); - expect(server.graphqlUrl).toMatch(/^http:\/\/localhost:\d+\/graphql$/); + // Server binds to 127.0.0.1 to avoid IPv6/IPv4 mismatch issues with supertest + expect(server.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/); + expect(server.graphqlUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/graphql$/); expect(server.port).toBeGreaterThan(0); }); it('should query users via HTTP', async () => { - const res = await query<{ allUsers: { nodes: Array<{ id: number; username: string }> } }>( - `query { allUsers { nodes { id username } } }` + const res = await query<{ users: { nodes: Array<{ id: number; username: string }> } }>( + `query { users { nodes { id username } } }` ); expect(res.data).toBeDefined(); - expect(res.data?.allUsers.nodes).toHaveLength(2); - expect(res.data?.allUsers.nodes[0].username).toBe('alice'); + expect(res.data?.users.nodes).toHaveLength(2); + expect(res.data?.users.nodes[0].username).toBe('alice'); }); it('should query posts via HTTP', async () => { - const res = await query<{ allPosts: { nodes: Array<{ id: number; title: string }> } }>( - `query { allPosts { nodes { id title } } }` + const res = await query<{ posts: { nodes: Array<{ id: number; title: string }> } }>( + `query { posts { nodes { id title } } }` ); expect(res.data).toBeDefined(); - expect(res.data?.allPosts.nodes).toHaveLength(3); + expect(res.data?.posts.nodes).toHaveLength(3); }); it('should support variables', async () => { @@ -87,14 +88,14 @@ describe('graphql-server-test', () => { const res = await request .post('/graphql') .set('Content-Type', 'application/json') - .send({ query: '{ allUsers { nodes { id } } }' }); + .send({ query: '{ users { nodes { id } } }' }); expect(res.status).toBe(200); - expect(res.body.data.allUsers.nodes).toHaveLength(2); + expect(res.body.data.users.nodes).toHaveLength(2); }); it('should snapshot query results', async () => { - const res = await query(`query { allUsers { nodes { username email } } }`); + const res = await query(`query { users { nodes { username email } } }`); expect(snapshot(res.data)).toMatchSnapshot(); }); }); @@ -138,7 +139,8 @@ describe('graphql-server-test', () => { }); it('should rollback changes between tests', async () => { - // Insert a new user + // Insert a new user using pg client (superuser, outside transaction) + // Note: pg client doesn't participate in db's transaction rollback await pg.query(`INSERT INTO app_public.users (username) VALUES ('charlie')`); // Verify it exists @@ -146,10 +148,11 @@ describe('graphql-server-test', () => { expect(parseInt(result.rows[0].count)).toBe(3); }); - it('should have rolled back the previous test changes', async () => { - // The user from the previous test should not exist + it('should verify data persists when using pg client', async () => { + // Since pg client doesn't participate in transaction rollback, + // the user from the previous test should still exist const result = await pg.query('SELECT COUNT(*) FROM app_public.users'); - expect(parseInt(result.rows[0].count)).toBe(2); + expect(parseInt(result.rows[0].count)).toBe(3); }); }); }); diff --git a/graphql/server-test/src/server.ts b/graphql/server-test/src/server.ts index 5bebf8fce..832b4fc6e 100644 --- a/graphql/server-test/src/server.ts +++ b/graphql/server-test/src/server.ts @@ -6,11 +6,12 @@ import type { ServerInfo, ServerOptions } from './types'; /** * Find an available port starting from the given port + * Uses 127.0.0.1 explicitly to avoid IPv6/IPv4 mismatch issues with supertest */ -const findAvailablePort = async (startPort: number): Promise => { +const findAvailablePort = async (startPort: number, host: string = '127.0.0.1'): Promise => { return new Promise((resolve, reject) => { const server = createServer(); - server.listen(startPort, () => { + server.listen(startPort, host, () => { const address = server.address(); const port = typeof address === 'object' && address ? address.port : startPort; server.close(() => resolve(port)); @@ -35,9 +36,11 @@ export const createTestServer = async ( opts: PgpmOptions, serverOpts: ServerOptions = {} ): Promise => { - const host = serverOpts.host ?? 'localhost'; + // Use 127.0.0.1 by default to avoid IPv6/IPv4 mismatch issues with supertest + // On some systems, 'localhost' resolves to ::1 (IPv6) but supertest connects to 127.0.0.1 (IPv4) + const host = serverOpts.host ?? '127.0.0.1'; const requestedPort = serverOpts.port ?? 0; - const port = requestedPort === 0 ? await findAvailablePort(5555) : requestedPort; + const port = requestedPort === 0 ? await findAvailablePort(5555, host) : requestedPort; // Merge server options into the PgpmOptions const serverConfig: PgpmOptions = { @@ -55,6 +58,16 @@ export const createTestServer = async ( // Start listening and get the HTTP server const httpServer: HttpServer = server.listen(); + // Wait for the server to actually be listening before getting the address + // The listen() call is async - it returns immediately but the server isn't ready yet + await new Promise((resolve) => { + if (httpServer.listening) { + resolve(); + } else { + httpServer.once('listening', () => resolve()); + } + }); + const actualPort = (httpServer.address() as { port: number }).port; const stop = async (): Promise => {