graphql-server-test 3.33.1 → 3.33.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -181,7 +181,6 @@ The `server.api` option provides full control over the GraphQL server configurat
181
181
 
182
182
  | Property | Type | Default | Description |
183
183
  |----------|------|---------|-------------|
184
- | `routingSchema` | `string` | `routing_public` | Schema that owns `resolve_route()` and the routing tables |
185
184
  | `exposedSchemas` | `string[]` | from `schemas` | Database schemas to expose (overridden by `schemas`) |
186
185
  | `anonRole` | `string` | from `authRole` | Anonymous role name (overridden by `authRole`) |
187
186
  | `roleName` | `string` | from `authRole` | Default role name (overridden by `authRole`) |
@@ -1,7 +1,41 @@
1
1
  import { getEnvOptions } from '@constructive-io/graphql-env';
2
+ import { randomBytes } from 'crypto';
3
+ import { pgCache } from 'pg-cache';
2
4
  import { getConnections as getPgConnections } from 'pgsql-test';
3
5
  import { createDevTestServer, createTestServer } from './server';
4
6
  import { createQueryFn, createSuperTestAgent } from './supertest';
7
+ /**
8
+ * The server connects as its own login, as in production: NOBYPASSRLS, a
9
+ * member of the request roles only, never of administrator. It holds the
10
+ * table/function privileges the seeded schemas grant it, so whatever RLS hides
11
+ * from it stays hidden and a read that silently relied on BYPASSRLS fails here.
12
+ */
13
+ const createServerLogin = async (pg) => {
14
+ const user = `server_test_${randomBytes(6).toString('hex')}`;
15
+ const password = randomBytes(18).toString('hex');
16
+ await pg.query(`
17
+ CREATE ROLE "${user}" LOGIN NOSUPERUSER NOBYPASSRLS PASSWORD '${password}' IN ROLE anonymous, authenticated;
18
+ GRANT CONNECT ON DATABASE "${pg.config.database}" TO "${user}";
19
+ DO $grant$
20
+ DECLARE s text;
21
+ BEGIN
22
+ FOR s IN
23
+ SELECT nspname FROM pg_namespace
24
+ WHERE nspname NOT LIKE 'pg\\_%' AND nspname <> 'information_schema'
25
+ LOOP
26
+ EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', s, '${user}');
27
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA %I TO %I', s, '${user}');
28
+ EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA %I TO %I', s, '${user}');
29
+ END LOOP;
30
+ END
31
+ $grant$;
32
+ `);
33
+ // The seed's pg-cache entry must not hand the server the seeding connection.
34
+ pgCache.delete(pg.config.database);
35
+ await pgCache.waitForDisposals();
36
+ return { user, password };
37
+ };
38
+ const liveSuites = new Set();
5
39
  /**
6
40
  * Creates connections with an HTTP server for SuperTest testing
7
41
  *
@@ -35,10 +69,11 @@ export const getConnections = async (input, seedAdapters) => {
35
69
  // Get database connections using pgsql-test
36
70
  const conn = await getPgConnections(input, seedAdapters);
37
71
  const { pg, db, teardown: dbTeardown } = conn;
72
+ const login = await createServerLogin(pg);
38
73
  // Build options for the HTTP server
39
74
  // Merge user-provided server.api options with convenience properties (schemas, authRole)
40
75
  const serverOpts = getEnvOptions({
41
- pg: pg.config,
76
+ pg: { ...pg.config, user: login.user, password: login.password },
42
77
  api: {
43
78
  // Start with user-provided api options from server.api
44
79
  ...input.server?.api,
@@ -57,9 +92,16 @@ export const getConnections = async (input, seedAdapters) => {
57
92
  : await createDevTestServer(serverOpts, input.server);
58
93
  // Create SuperTest agent
59
94
  const request = createSuperTestAgent(server);
60
- // Combined teardown function
95
+ const suite = { server, pg, user: login.user };
96
+ liveSuites.add(suite);
97
+ // pgsql-test's teardown closes every database this process opened, so the
98
+ // first teardown also releases every other live suite's server and login.
61
99
  const teardown = async () => {
62
- await server.stop();
100
+ for (const live of [...liveSuites]) {
101
+ liveSuites.delete(live);
102
+ await live.server.stop();
103
+ await live.pg.query(`DROP OWNED BY "${live.user}"; DROP ROLE "${live.user}"`);
104
+ }
63
105
  await dbTeardown();
64
106
  };
65
107
  return {
@@ -2,9 +2,43 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getConnections = void 0;
4
4
  const graphql_env_1 = require("@constructive-io/graphql-env");
5
+ const crypto_1 = require("crypto");
6
+ const pg_cache_1 = require("pg-cache");
5
7
  const pgsql_test_1 = require("pgsql-test");
6
8
  const server_1 = require("./server");
7
9
  const supertest_1 = require("./supertest");
10
+ /**
11
+ * The server connects as its own login, as in production: NOBYPASSRLS, a
12
+ * member of the request roles only, never of administrator. It holds the
13
+ * table/function privileges the seeded schemas grant it, so whatever RLS hides
14
+ * from it stays hidden and a read that silently relied on BYPASSRLS fails here.
15
+ */
16
+ const createServerLogin = async (pg) => {
17
+ const user = `server_test_${(0, crypto_1.randomBytes)(6).toString('hex')}`;
18
+ const password = (0, crypto_1.randomBytes)(18).toString('hex');
19
+ await pg.query(`
20
+ CREATE ROLE "${user}" LOGIN NOSUPERUSER NOBYPASSRLS PASSWORD '${password}' IN ROLE anonymous, authenticated;
21
+ GRANT CONNECT ON DATABASE "${pg.config.database}" TO "${user}";
22
+ DO $grant$
23
+ DECLARE s text;
24
+ BEGIN
25
+ FOR s IN
26
+ SELECT nspname FROM pg_namespace
27
+ WHERE nspname NOT LIKE 'pg\\_%' AND nspname <> 'information_schema'
28
+ LOOP
29
+ EXECUTE format('GRANT USAGE ON SCHEMA %I TO %I', s, '${user}');
30
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA %I TO %I', s, '${user}');
31
+ EXECUTE format('GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA %I TO %I', s, '${user}');
32
+ END LOOP;
33
+ END
34
+ $grant$;
35
+ `);
36
+ // The seed's pg-cache entry must not hand the server the seeding connection.
37
+ pg_cache_1.pgCache.delete(pg.config.database);
38
+ await pg_cache_1.pgCache.waitForDisposals();
39
+ return { user, password };
40
+ };
41
+ const liveSuites = new Set();
8
42
  /**
9
43
  * Creates connections with an HTTP server for SuperTest testing
10
44
  *
@@ -38,10 +72,11 @@ const getConnections = async (input, seedAdapters) => {
38
72
  // Get database connections using pgsql-test
39
73
  const conn = await (0, pgsql_test_1.getConnections)(input, seedAdapters);
40
74
  const { pg, db, teardown: dbTeardown } = conn;
75
+ const login = await createServerLogin(pg);
41
76
  // Build options for the HTTP server
42
77
  // Merge user-provided server.api options with convenience properties (schemas, authRole)
43
78
  const serverOpts = (0, graphql_env_1.getEnvOptions)({
44
- pg: pg.config,
79
+ pg: { ...pg.config, user: login.user, password: login.password },
45
80
  api: {
46
81
  // Start with user-provided api options from server.api
47
82
  ...input.server?.api,
@@ -60,9 +95,16 @@ const getConnections = async (input, seedAdapters) => {
60
95
  : await (0, server_1.createDevTestServer)(serverOpts, input.server);
61
96
  // Create SuperTest agent
62
97
  const request = (0, supertest_1.createSuperTestAgent)(server);
63
- // Combined teardown function
98
+ const suite = { server, pg, user: login.user };
99
+ liveSuites.add(suite);
100
+ // pgsql-test's teardown closes every database this process opened, so the
101
+ // first teardown also releases every other live suite's server and login.
64
102
  const teardown = async () => {
65
- await server.stop();
103
+ for (const live of [...liveSuites]) {
104
+ liveSuites.delete(live);
105
+ await live.server.stop();
106
+ await live.pg.query(`DROP OWNED BY "${live.user}"; DROP ROLE "${live.user}"`);
107
+ }
66
108
  await dbTeardown();
67
109
  };
68
110
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphql-server-test",
3
- "version": "3.33.1",
3
+ "version": "3.33.3",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Constructive GraphQL Server Testing with SuperTest HTTP requests",
6
6
  "main": "index.js",
@@ -31,8 +31,8 @@
31
31
  "devDependencies": {
32
32
  "@0no-co/graphql.web": "^1.3.3",
33
33
  "@agentic-kit/ollama": "2.16.0",
34
- "@constructive-io/graphql-codegen": "^5.24.1",
35
- "@constructive-io/graphql-query": "^4.21.1",
34
+ "@constructive-io/graphql-codegen": "^5.24.3",
35
+ "@constructive-io/graphql-query": "^4.21.3",
36
36
  "@constructive-io/s3-utils": "^2.35.0",
37
37
  "@pgpmjs/core": "^7.34.0",
38
38
  "@simplewebauthn/server": "13.3.2",
@@ -41,23 +41,25 @@
41
41
  "@types/supertest": "^7.2.1",
42
42
  "appstash": "^0.8.0",
43
43
  "gql-ast": "^3.26.0",
44
+ "graphile-cache": "^4.16.1",
44
45
  "inquirerer": "^4.9.4",
45
46
  "makage": "^0.8.0",
46
47
  "nested-obj": "^0.2.3"
47
48
  },
48
49
  "dependencies": {
49
- "@constructive-io/graphql-dev-server": "^3.21.1",
50
- "@constructive-io/graphql-env": "^3.37.0",
51
- "@constructive-io/graphql-server": "^5.32.1",
52
- "@constructive-io/graphql-types": "^3.36.0",
50
+ "@constructive-io/express-context": "^0.35.1",
51
+ "@constructive-io/graphql-dev-server": "^3.21.3",
52
+ "@constructive-io/graphql-env": "^3.37.2",
53
+ "@constructive-io/graphql-server": "^5.32.3",
54
+ "@constructive-io/graphql-types": "^3.36.1",
53
55
  "@constructive-io/upload-client": "0.29.0",
54
56
  "@pgpmjs/types": "^2.58.0",
55
57
  "express": "^5.2.1",
56
- "graphile-schema": "^2.20.1",
58
+ "graphile-schema": "^2.20.3",
57
59
  "graphql": "16.13.0",
58
60
  "pg": "^8.21.0",
59
61
  "pg-cache": "^3.30.0",
60
- "pgsql-test": "^5.18.0",
62
+ "pgsql-test": "^5.18.1",
61
63
  "supertest": "^7.0.0"
62
64
  },
63
65
  "keywords": [
@@ -71,5 +73,5 @@
71
73
  "e2e",
72
74
  "server"
73
75
  ],
74
- "gitHead": "6cbaa8605270a2e019439fd9f444a35b3f2a9de0"
76
+ "gitHead": "0b4ffa11a6d7958ba1068ce8e19616d4b018c0d6"
75
77
  }