import { describe, it } from 'mocha';
import { parse } from '../../language/parser.js';
import type { GraphQLSchema } from '../../type/schema.js';
import { extendSchema } from '../../utilities/extendSchema.js';
import { UniqueDirectivesPerLocationRule } from '../rules/UniqueDirectivesPerLocationRule.js';
import {
expectSDLValidationErrors,
expectValidationErrorsWithSchema,
testSchema,
} from './harness.js';
const extensionSDL = `
directive @directive on FIELD | FRAGMENT_DEFINITION
directive @directiveA on FIELD | FRAGMENT_DEFINITION
directive @directiveB on FIELD | FRAGMENT_DEFINITION
directive @repeatable repeatable on FIELD | FRAGMENT_DEFINITION
`;
const schemaWithDirectives = extendSchema(testSchema, parse(extensionSDL));
function expectErrors(queryStr: string) {
return expectValidationErrorsWithSchema(
schemaWithDirectives,
UniqueDirectivesPerLocationRule,
queryStr,
);
}
function expectValid(queryStr: string) {
expectErrors(queryStr).toDeepEqual([]);
}
function expectSDLErrors(sdlStr: string, schema?: GraphQLSchema) {
return expectSDLValidationErrors(
schema,
UniqueDirectivesPerLocationRule,
sdlStr,
);
}
describe('Validate: Directives Are Unique Per Location', () => {
it('no directives', () => {
expectValid(`
fragment Test on Type {
field
}
`);
});
it('unique directives in different locations', () => {
expectValid(`
fragment Test on Type @directiveA {
field @directiveB
}
`);
});
it('unique directives in same locations', () => {
expectValid(`
fragment Test on Type @directiveA @directiveB {
field @directiveA @directiveB
}
`);
});
it('same directives in different locations', () => {
expectValid(`
fragment Test on Type @directiveA {
field @directiveA
}
`);
});
it('same directives in similar locations', () => {
expectValid(`
fragment Test on Type {
field @directive
field @directive
}
`);
});
it('repeatable directives in same location', () => {
expectValid(`
fragment Test on Type @repeatable @repeatable {
field @repeatable @repeatable
}
`);
});
it('unknown directives must be ignored', () => {
expectValid(`
type Test @unknown @unknown {
field: String! @unknown @unknown
}
extend type Test @unknown {
anotherField: String!
}
`);
});
it('duplicate directives in one location', () => {
expectErrors(`
fragment Test on Type {
field @directive @directive
}
`).toDeepEqual([
{
message:
'The directive "@directive" can only be used once at this location.',
locations: [
{ line: 3, column: 15 },
{ line: 3, column: 26 },
],
},
]);
});
it('many duplicate directives in one location', () => {
expectErrors(`
fragment Test on Type {
field @directive @directive @directive
}
`).toDeepEqual([
{
message:
'The directive "@directive" can only be used once at this location.',
locations: [
{ line: 3, column: 15 },
{ line: 3, column: 26 },
],
},
{
message:
'The directive "@directive" can only be used once at this location.',
locations: [
{ line: 3, column: 15 },
{ line: 3, column: 37 },
],
},
]);
});
it('different duplicate directives in one location', () => {
expectErrors(`
fragment Test on Type {
field @directiveA @directiveB @directiveA @directiveB
}
`).toDeepEqual([
{
message:
'The directive "@directiveA" can only be used once at this location.',
locations: [
{ line: 3, column: 15 },
{ line: 3, column: 39 },
],
},
{
message:
'The directive "@directiveB" can only be used once at this location.',
locations: [
{ line: 3, column: 27 },
{ line: 3, column: 51 },
],
},
]);
});
it('duplicate directives in many locations', () => {
expectErrors(`
fragment Test on Type @directive @directive {
field @directive @directive
}
`).toDeepEqual([
{
message:
'The directive "@directive" can only be used once at this location.',
locations: [
{ line: 2, column: 29 },
{ line: 2, column: 40 },
],
},
{
message:
'The directive "@directive" can only be used once at this location.',
locations: [
{ line: 3, column: 15 },
{ line: 3, column: 26 },
],
},
]);
});
it('duplicate directives on SDL definitions', () => {
expectSDLErrors(`
directive @nonRepeatable on
SCHEMA | SCALAR | OBJECT | INTERFACE | UNION | INPUT_OBJECT
schema @nonRepeatable @nonRepeatable { query: Dummy }
scalar TestScalar @nonRepeatable @nonRepeatable
type TestObject @nonRepeatable @nonRepeatable
interface TestInterface @nonRepeatable @nonRepeatable
union TestUnion @nonRepeatable @nonRepeatable
input TestInput @nonRepeatable @nonRepeatable
`).toDeepEqual([
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 5, column: 14 },
{ line: 5, column: 29 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 7, column: 25 },
{ line: 7, column: 40 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 8, column: 23 },
{ line: 8, column: 38 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 9, column: 31 },
{ line: 9, column: 46 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 10, column: 23 },
{ line: 10, column: 38 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 11, column: 23 },
{ line: 11, column: 38 },
],
},
]);
});
it('duplicate directives on SDL extensions', () => {
expectSDLErrors(`
directive @nonRepeatable on
SCHEMA | SCALAR | OBJECT | INTERFACE | UNION | INPUT_OBJECT
extend schema @nonRepeatable @nonRepeatable
extend scalar TestScalar @nonRepeatable @nonRepeatable
extend type TestObject @nonRepeatable @nonRepeatable
extend interface TestInterface @nonRepeatable @nonRepeatable
extend union TestUnion @nonRepeatable @nonRepeatable
extend input TestInput @nonRepeatable @nonRepeatable
`).toDeepEqual([
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 5, column: 21 },
{ line: 5, column: 36 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 7, column: 32 },
{ line: 7, column: 47 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 8, column: 30 },
{ line: 8, column: 45 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 9, column: 38 },
{ line: 9, column: 53 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 10, column: 30 },
{ line: 10, column: 45 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 11, column: 30 },
{ line: 11, column: 45 },
],
},
]);
});
it('duplicate directives between SDL definitions and extensions', () => {
expectSDLErrors(`
directive @nonRepeatable on SCHEMA
schema @nonRepeatable { query: Dummy }
extend schema @nonRepeatable
`).toDeepEqual([
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 4, column: 14 },
{ line: 5, column: 21 },
],
},
]);
expectSDLErrors(`
directive @nonRepeatable on SCALAR
scalar TestScalar @nonRepeatable
extend scalar TestScalar @nonRepeatable
scalar TestScalar @nonRepeatable
`).toDeepEqual([
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 4, column: 25 },
{ line: 5, column: 32 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 4, column: 25 },
{ line: 6, column: 25 },
],
},
]);
expectSDLErrors(`
directive @nonRepeatable on OBJECT
extend type TestObject @nonRepeatable
type TestObject @nonRepeatable
extend type TestObject @nonRepeatable
`).toDeepEqual([
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 4, column: 30 },
{ line: 5, column: 23 },
],
},
{
message:
'The directive "@nonRepeatable" can only be used once at this location.',
locations: [
{ line: 4, column: 30 },
{ line: 6, column: 30 },
],
},
]);
});
});