Skip to content

Commit 5e8ebd8

Browse files
author
Dimitri POSTOLOV
authored
3️⃣ Test schema loaders, fix Expected signal to be an instanceof AbortSignal error when UrlLoader is used (#542)
1 parent 5065482 commit 5e8ebd8

File tree

10 files changed

+153
-21
lines changed

10 files changed

+153
-21
lines changed

.changeset/wild-rules-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': patch
3+
---
4+
5+
add tests for schema loaders

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
"pre-commit": "yarn generate:docs"
1212
},
1313
"scripts": {
14-
"generate:docs": "ts-node --compiler-options='{\"module\":\"commonjs\"}' scripts/generate-docs.ts",
14+
"generate:docs": "ts-node scripts/generate-docs.ts",
1515
"postinstall": "patch-package",
16-
"lint": "eslint --config .eslintrc.json --ext .ts .",
16+
"lint": "eslint --config .eslintrc.json --ext ts,js .",
1717
"prebuild": "rimraf packages/*/dist",
1818
"transpile-ts": "tsc --project tsconfig.json",
1919
"build": "yarn transpile-ts && bob build",
2020
"postbuild": "cp -r README.md docs ./packages/plugin/dist/",
21-
"test": "jest --no-watchman --forceExit --noStackTrace",
21+
"test": "jest --no-watchman --forceExit --noStackTrace --detectOpenHandles",
2222
"prerelease": "yarn build",
2323
"release": "changeset publish",
2424
"release:canary": "(node scripts/canary-release.js && yarn build && yarn changeset publish --tag alpha) || echo Skipping Canary...",

packages/plugin/src/graphql-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GraphQLConfig, GraphQLExtensionDeclaration, loadConfigSync } from 'graphql-config';
1+
import { GraphQLConfig, GraphQLExtensionDeclaration, loadConfigSync, SchemaPointer } from 'graphql-config';
22
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
33
import { ParserOptions } from './types';
44

@@ -26,7 +26,7 @@ export function loadGraphqlConfig(options: ParserOptions): GraphQLConfig {
2626
config: options.projects
2727
? { projects: options.projects }
2828
: {
29-
schema: options.schema || '', // if `options.schema` is `undefined` will throw error `Project 'default' not found`
29+
schema: (options.schema || '') as SchemaPointer, // if `schema` is `undefined` will throw error `Project 'default' not found`
3030
documents: options.documents || options.operations,
3131
extensions: options.extensions,
3232
include: options.include,

packages/plugin/src/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getOnDiskFilepath } from './utils';
66

77
const schemaCache: Map<string, GraphQLSchema> = new Map();
88

9-
export function getSchema(options: ParserOptions, gqlConfig: GraphQLConfig): GraphQLSchema | null {
9+
export function getSchema(options: ParserOptions = {}, gqlConfig: GraphQLConfig): GraphQLSchema | null {
1010
const realFilepath = options.filePath ? getOnDiskFilepath(options.filePath) : null;
1111
const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
1212
const schemaKey = asArray(projectForFile.schema)
@@ -21,7 +21,7 @@ export function getSchema(options: ParserOptions, gqlConfig: GraphQLConfig): Gra
2121
return schemaCache.get(schemaKey);
2222
}
2323

24-
const schema = projectForFile.getSchemaSync();
24+
const schema = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', options.schemaOptions);
2525
schemaCache.set(schemaKey, schema);
2626

2727
return schema;

packages/plugin/src/sibling-operations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export type SiblingOperations = {
3232
const operationsCache: Map<string, Source[]> = new Map();
3333
const siblingOperationsCache: Map<Source[], SiblingOperations> = new Map();
3434

35-
const getSiblings = (filePath: string, gqlConfig: GraphQLConfig): Source[] | null => {
35+
const getSiblings = (filePath: string, gqlConfig: GraphQLConfig): Source[] => {
3636
const realFilepath = filePath ? getOnDiskFilepath(filePath) : null;
3737
const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
3838
const documentsKey = asArray(projectForFile.documents)

packages/plugin/src/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ import { SiblingOperations } from './sibling-operations';
88
import { getReachableTypes, getUsedFields } from './graphql-ast';
99

1010
export interface ParserOptions {
11-
schema?: SchemaPointer;
11+
schema?: SchemaPointer | Record<string, { headers: Record<string, string> }>;
1212
documents?: DocumentPointer;
1313
operations?: DocumentPointer; // legacy
1414
extensions?: IExtensions;
1515
include?: string | string[];
1616
exclude?: string | string[];
1717
projects?: Record<string, IGraphQLProject>;
18-
schemaOptions?: Omit<GraphQLParseOptions, 'assumeValidSDL'>;
18+
schemaOptions?: Omit<GraphQLParseOptions, 'assumeValidSDL'> & {
19+
headers: Record<string, string>;
20+
};
1921
graphQLParserOptions?: Omit<GraphQLParseOptions, 'noLocation'>;
2022
skipGraphQLConfig?: boolean;
2123
filePath?: string;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { readFileSync } from 'fs';
2+
import { resolve } from 'path';
3+
import { createServer, Server, IncomingMessage, ServerResponse } from 'http';
4+
import { buildSchema, getIntrospectionQuery, graphqlSync } from 'graphql';
5+
6+
const sdlSchema = readFileSync(resolve(__dirname, 'user-schema.graphql'), 'utf8');
7+
const graphqlSchemaObj = buildSchema(sdlSchema);
8+
const introspectionQueryResult = graphqlSync(graphqlSchemaObj, getIntrospectionQuery());
9+
10+
class TestGraphQLServer {
11+
private server: Server;
12+
private base: string;
13+
14+
constructor(private port = 1337) {
15+
this.server = createServer(this.router.bind(this));
16+
this.base = `http://localhost:${this.port}`;
17+
}
18+
19+
start(): Promise<{ url: string }> {
20+
return new Promise(resolve => {
21+
this.server.listen(this.port, () => {
22+
resolve({ url: this.base });
23+
});
24+
});
25+
}
26+
27+
stop(): Promise<void> {
28+
return new Promise((resolve, reject) => {
29+
this.server.close(err => {
30+
err ? reject(err) : resolve();
31+
});
32+
});
33+
}
34+
35+
private async router(req: IncomingMessage, res: ServerResponse): Promise<void> {
36+
const { pathname } = new URL(req.url, this.base);
37+
38+
if (pathname === '/') {
39+
const { query } = await this.parseData(req);
40+
if (query.includes('query IntrospectionQuery')) {
41+
res.end(JSON.stringify(introspectionQueryResult));
42+
}
43+
} else if (pathname === '/my-headers') {
44+
res.end(JSON.stringify(req.headers));
45+
}
46+
}
47+
48+
parseData(req: IncomingMessage): Promise<any | string> {
49+
return new Promise(resolve => {
50+
let data = '';
51+
req.on('data', chunk => {
52+
data += chunk;
53+
});
54+
req.on('end', () => {
55+
if (req.headers['content-type'] === 'application/json') {
56+
resolve(JSON.parse(data));
57+
} else {
58+
resolve(data);
59+
}
60+
});
61+
});
62+
}
63+
}
64+
65+
const graphqlServer = new TestGraphQLServer();
66+
67+
graphqlServer.start().then(({ url }) => {
68+
// eslint-disable-next-line no-console
69+
console.log(url);
70+
});

packages/plugin/tests/mocks/unique-fragment.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
// eslint-disable-next-line no-undef
12
const USER_FIELDS = gql`
23
fragment UserFields on User {
34
id
45
}
56
`;
67

8+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
79
const GET_USER = /* GraphQL */ `
810
query User {
911
user {

packages/plugin/tests/schema.spec.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { resolve } from 'path';
22
import { readFileSync } from 'fs';
3+
import { spawn } from 'child_process';
34
import { GraphQLSchema, printSchema } from 'graphql';
45
import { getSchema } from '../src/schema';
56
import { loadGraphqlConfig } from '../src/graphql-config';
@@ -37,10 +38,61 @@ describe('schema', () => {
3738
});
3839
});
3940

40-
// TODO: make works when url-loader will be updated to v7
41-
// describe('UrlLoader', () => {
42-
// it('should load schema from URL', () => {
43-
// testSchema(url);
44-
// });
45-
// });
41+
describe('UrlLoader', () => {
42+
let local;
43+
let url;
44+
45+
beforeAll(done => {
46+
const tsNodeCommand = resolve(process.cwd(), 'node_modules/.bin/ts-node');
47+
const serverPath = resolve(__dirname, 'mocks/graphql-server.ts');
48+
49+
// Import `TestGraphQLServer` and run it in this file will don't work
50+
// because `@graphql-tools/url-loader` under the hood uses `sync-fetch` package that uses
51+
// `child_process.execFileSync` that block Node.js event loop
52+
local = spawn(tsNodeCommand, [serverPath]);
53+
local.stdout.on('data', chunk => {
54+
url = chunk.toString().trimRight();
55+
done();
56+
});
57+
});
58+
59+
afterAll(done => {
60+
local.on('close', done);
61+
local.kill();
62+
});
63+
64+
it('should load schema from URL', () => {
65+
testSchema(url);
66+
});
67+
68+
it('should passe headers', () => {
69+
expect.assertions(2);
70+
const schemaUrl = `${url}/my-headers`;
71+
const schemaOptions = {
72+
headers: {
73+
Authorization: 'Bearer Foo',
74+
},
75+
};
76+
77+
try {
78+
// https://graphql-config.com/schema#passing-headers
79+
const gqlConfig = loadGraphqlConfig({
80+
schema: {
81+
[schemaUrl]: schemaOptions,
82+
},
83+
});
84+
getSchema(undefined, gqlConfig);
85+
} catch (e) {
86+
expect(e.message).toMatch('"authorization":"Bearer Foo"');
87+
}
88+
89+
try {
90+
// https://github.com/dotansimha/graphql-eslint/blob/master/docs/parser-options.md#schemaoptions
91+
const gqlConfig = loadGraphqlConfig({ schema: schemaUrl });
92+
getSchema({ schemaOptions }, gqlConfig);
93+
} catch (e) {
94+
expect(e.message).toMatch('"authorization":"Bearer Foo"');
95+
}
96+
});
97+
});
4698
});

tsconfig.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@
2727
}
2828
},
2929
"include": ["packages"],
30-
"exclude": [
31-
"**/dist",
32-
"**/temp",
33-
"**/tests"
34-
]
30+
"exclude": ["**/dist", "**/temp", "**/tests"],
31+
"ts-node": {
32+
"compilerOptions": {
33+
"module": "commonjs"
34+
}
35+
}
3536
}

0 commit comments

Comments
 (0)