diff --git a/src/execution/__tests__/variables-test.js b/src/execution/__tests__/variables-test.js index 49596c5bfb..ecc60e14ed 100644 --- a/src/execution/__tests__/variables-test.js +++ b/src/execution/__tests__/variables-test.js @@ -648,13 +648,6 @@ describe('Execute: Handles inputs', () => { expect(result.errors[0].originalError).not.to.equal(undefined); }); - it('serializing an array via GraphQLString throws TypeError', () => { - expect(() => GraphQLString.serialize([1, 2, 3])).to.throw( - TypeError, - 'String cannot represent an array value: [1,2,3]', - ); - }); - it('reports error for non-provided variables for non-nullable inputs', () => { // Note: this test would typically fail validation before encountering // this execution error, however for queries which previously validated diff --git a/src/type/__tests__/serialization-test.js b/src/type/__tests__/serialization-test.js index 78bed4f89e..d6b832765b 100644 --- a/src/type/__tests__/serialization-test.js +++ b/src/type/__tests__/serialization-test.js @@ -5,18 +5,27 @@ * LICENSE file in the root directory of this source tree. */ -import { GraphQLInt, GraphQLFloat, GraphQLString, GraphQLBoolean } from '../'; +import { + GraphQLInt, + GraphQLID, + GraphQLFloat, + GraphQLString, + GraphQLBoolean, +} from '../'; import { describe, it } from 'mocha'; import { expect } from 'chai'; describe('Type System: Scalar coercion', () => { - it('serializes output int', () => { + it('serializes output as Int', () => { expect(GraphQLInt.serialize(1)).to.equal(1); expect(GraphQLInt.serialize('123')).to.equal(123); expect(GraphQLInt.serialize(0)).to.equal(0); expect(GraphQLInt.serialize(-1)).to.equal(-1); expect(GraphQLInt.serialize(1e5)).to.equal(100000); + expect(GraphQLInt.serialize(false)).to.equal(0); + expect(GraphQLInt.serialize(true)).to.equal(1); + // The GraphQL specification does not allow serializing non-integer values // as Int to avoid accidental data loss. expect(() => GraphQLInt.serialize(0.1)).to.throw( @@ -49,17 +58,19 @@ describe('Type System: Scalar coercion', () => { expect(() => GraphQLInt.serialize('one')).to.throw( 'Int cannot represent non 32-bit signed integer value: one', ); - expect(GraphQLInt.serialize(false)).to.equal(0); - expect(GraphQLInt.serialize(true)).to.equal(1); + // Doesn't represent number expect(() => GraphQLInt.serialize('')).to.throw( 'Int cannot represent non 32-bit signed integer value: (empty string)', ); expect(() => GraphQLInt.serialize(NaN)).to.throw( 'Int cannot represent non 32-bit signed integer value: NaN', ); + expect(() => GraphQLInt.serialize([5])).to.throw( + 'Int cannot represent an array value: [5]', + ); }); - it('serializes output float', () => { + it('serializes output as Float', () => { expect(GraphQLFloat.serialize(1)).to.equal(1.0); expect(GraphQLFloat.serialize(0)).to.equal(0.0); expect(GraphQLFloat.serialize('123.5')).to.equal(123.5); @@ -74,30 +85,42 @@ describe('Type System: Scalar coercion', () => { expect(() => GraphQLFloat.serialize(NaN)).to.throw( 'Float cannot represent non numeric value: NaN', ); - expect(() => GraphQLFloat.serialize('one')).to.throw( 'Float cannot represent non numeric value: one', ); - expect(() => GraphQLFloat.serialize('')).to.throw( 'Float cannot represent non numeric value: (empty string)', ); + expect(() => GraphQLFloat.serialize([5])).to.throw( + 'Float cannot represent an array value: [5]', + ); }); - it('serializes output strings', () => { - expect(GraphQLString.serialize('string')).to.equal('string'); - expect(GraphQLString.serialize(1)).to.equal('1'); - expect(GraphQLString.serialize(-1.1)).to.equal('-1.1'); - expect(GraphQLString.serialize(true)).to.equal('true'); - expect(GraphQLString.serialize(false)).to.equal('false'); - }); + for (const scalar of [GraphQLString, GraphQLID]) { + it(`serializes output as ${scalar}`, () => { + expect(scalar.serialize('string')).to.equal('string'); + expect(scalar.serialize(1)).to.equal('1'); + expect(scalar.serialize(-1.1)).to.equal('-1.1'); + expect(scalar.serialize(true)).to.equal('true'); + expect(scalar.serialize(false)).to.equal('false'); - it('serializes output boolean', () => { + expect(() => scalar.serialize([1])).to.throw( + 'String cannot represent an array value: [1]', + ); + }); + } + + it('serializes output as Boolean', () => { expect(GraphQLBoolean.serialize('string')).to.equal(true); + expect(GraphQLBoolean.serialize('false')).to.equal(true); expect(GraphQLBoolean.serialize('')).to.equal(false); expect(GraphQLBoolean.serialize(1)).to.equal(true); expect(GraphQLBoolean.serialize(0)).to.equal(false); expect(GraphQLBoolean.serialize(true)).to.equal(true); expect(GraphQLBoolean.serialize(false)).to.equal(false); + + expect(() => GraphQLBoolean.serialize([false])).to.throw( + 'Boolean cannot represent an array value: [false]', + ); }); }); diff --git a/src/type/scalars.js b/src/type/scalars.js index c60bcc8cdc..9bf35443bf 100644 --- a/src/type/scalars.js +++ b/src/type/scalars.js @@ -18,7 +18,12 @@ import { Kind } from '../language/kinds'; const MAX_INT = 2147483647; const MIN_INT = -2147483648; -function coerceInt(value: mixed): ?number { +function coerceInt(value: mixed): number { + if (Array.isArray(value)) { + throw new TypeError( + `Int cannot represent an array value: [${String(value)}]`, + ); + } if (value === '') { throw new TypeError( 'Int cannot represent non 32-bit signed integer value: (empty string)', @@ -57,7 +62,12 @@ export const GraphQLInt = new GraphQLScalarType({ }, }); -function coerceFloat(value: mixed): ?number { +function coerceFloat(value: mixed): number { + if (Array.isArray(value)) { + throw new TypeError( + `Float cannot represent an array value: [${String(value)}]`, + ); + } if (value === '') { throw new TypeError( 'Float cannot represent non numeric value: (empty string)', @@ -87,7 +97,7 @@ export const GraphQLFloat = new GraphQLScalarType({ }, }); -function coerceString(value: mixed): ?string { +function coerceString(value: mixed): string { if (Array.isArray(value)) { throw new TypeError( `String cannot represent an array value: [${String(value)}]`, @@ -109,11 +119,20 @@ export const GraphQLString = new GraphQLScalarType({ }, }); +function coerceBoolean(value: mixed): boolean { + if (Array.isArray(value)) { + throw new TypeError( + `Boolean cannot represent an array value: [${String(value)}]`, + ); + } + return Boolean(value); +} + export const GraphQLBoolean = new GraphQLScalarType({ name: 'Boolean', description: 'The `Boolean` scalar type represents `true` or `false`.', - serialize: Boolean, - parseValue: Boolean, + serialize: coerceBoolean, + parseValue: coerceBoolean, parseLiteral(ast) { return ast.kind === Kind.BOOLEAN ? ast.value : undefined; }, @@ -127,8 +146,8 @@ export const GraphQLID = new GraphQLScalarType({ 'response as a String; however, it is not intended to be human-readable. ' + 'When expected as an input type, any string (such as `"4"`) or integer ' + '(such as `4`) input value will be accepted as an ID.', - serialize: String, - parseValue: String, + serialize: coerceString, + parseValue: coerceString, parseLiteral(ast) { return ast.kind === Kind.STRING || ast.kind === Kind.INT ? ast.value diff --git a/src/utilities/__tests__/coerceValue-test.js b/src/utilities/__tests__/coerceValue-test.js index 3bad8ab0c2..6950ca452a 100644 --- a/src/utilities/__tests__/coerceValue-test.js +++ b/src/utilities/__tests__/coerceValue-test.js @@ -9,6 +9,7 @@ import { describe, it } from 'mocha'; import { expect } from 'chai'; import { coerceValue } from '../coerceValue'; import { + GraphQLID, GraphQLInt, GraphQLFloat, GraphQLString, @@ -17,126 +18,119 @@ import { GraphQLNonNull, } from '../../type'; -function expectNoErrors(result) { +function expectValue(result) { expect(result.errors).to.equal(undefined); - expect(result.value).not.to.equal(undefined); + return expect(result.value); } -function expectError(result, expected) { - const messages = result.errors && result.errors.map(error => error.message); - expect(messages).to.deep.equal([expected]); +function expectErrors(result) { expect(result.value).to.equal(undefined); + const messages = result.errors && result.errors.map(error => error.message); + return expect(messages); } describe('coerceValue', () => { - it('coercing an array to GraphQLString produces an error', () => { - const result = coerceValue([1, 2, 3], GraphQLString); - expectError( - result, - 'Expected type String; String cannot represent an array value: [1,2,3]', - ); - expect(result.errors[0].originalError.message).to.equal( - 'String cannot represent an array value: [1,2,3]', - ); - }); + for (const scalar of [GraphQLString, GraphQLID]) { + describe(`for GraphQL${scalar}`, () => { + it('returns error for array input as string', () => { + const result = coerceValue([1, 2, 3], scalar); + expectErrors(result).to.deep.equal([ + `Expected type ${scalar}; String cannot represent an array value: [1,2,3]`, + ]); + }); + }); + } describe('for GraphQLInt', () => { it('returns no error for int input', () => { const result = coerceValue('1', GraphQLInt); - expectNoErrors(result); + expectValue(result).to.equal(1); }); it('returns no error for negative int input', () => { const result = coerceValue('-1', GraphQLInt); - expectNoErrors(result); + expectValue(result).to.equal(-1); }); it('returns no error for exponent input', () => { const result = coerceValue('1e3', GraphQLInt); - expectNoErrors(result); + expectValue(result).to.equal(1000); }); it('returns a single error for empty value', () => { const result = coerceValue(null, GraphQLInt); - expectNoErrors(result); + expectValue(result).to.equal(null); }); it('returns a single error for empty value', () => { const result = coerceValue('', GraphQLInt); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Expected type Int; Int cannot represent non 32-bit signed integer value: (empty string)', - ); + ]); }); it('returns error for float input as int', () => { const result = coerceValue('1.5', GraphQLInt); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Expected type Int; Int cannot represent non-integer value: 1.5', - ); + ]); }); it('returns a single error for char input', () => { const result = coerceValue('a', GraphQLInt); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Expected type Int; Int cannot represent non 32-bit signed integer value: a', - ); + ]); }); it('returns a single error for char input', () => { const result = coerceValue('meow', GraphQLInt); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Expected type Int; Int cannot represent non 32-bit signed integer value: meow', - ); + ]); }); }); describe('for GraphQLFloat', () => { it('returns no error for int input', () => { const result = coerceValue('1', GraphQLFloat); - expectNoErrors(result); + expectValue(result).to.equal(1); }); it('returns no error for exponent input', () => { const result = coerceValue('1e3', GraphQLFloat); - expectNoErrors(result); + expectValue(result).to.equal(1000); }); it('returns no error for float input', () => { const result = coerceValue('1.5', GraphQLFloat); - expectNoErrors(result); + expectValue(result).to.equal(1.5); }); it('returns a single error for empty value', () => { const result = coerceValue(null, GraphQLFloat); - expectNoErrors(result); + expectValue(result).to.equal(null); }); it('returns a single error for empty value', () => { const result = coerceValue('', GraphQLFloat); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Expected type Float; Float cannot represent non numeric value: (empty string)', - ); + ]); }); it('returns a single error for char input', () => { const result = coerceValue('a', GraphQLFloat); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Expected type Float; Float cannot represent non numeric value: a', - ); + ]); }); it('returns a single error for char input', () => { const result = coerceValue('meow', GraphQLFloat); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Expected type Float; Float cannot represent non numeric value: meow', - ); + ]); }); }); @@ -151,25 +145,25 @@ describe('coerceValue', () => { it('returns no error for a known enum name', () => { const fooResult = coerceValue('FOO', TestEnum); - expectNoErrors(fooResult); - expect(fooResult.value).to.equal('InternalFoo'); + expectValue(fooResult).to.equal('InternalFoo'); const barResult = coerceValue('BAR', TestEnum); - expectNoErrors(barResult); - expect(barResult.value).to.equal(123456789); + expectValue(barResult).to.equal(123456789); }); it('results error for misspelled enum value', () => { const result = coerceValue('foo', TestEnum); - expectError(result, 'Expected type TestEnum; did you mean FOO?'); + expectErrors(result).to.deep.equal([ + 'Expected type TestEnum; did you mean FOO?', + ]); }); it('results error for incorrect value type', () => { const result1 = coerceValue(123, TestEnum); - expectError(result1, 'Expected type TestEnum.'); + expectErrors(result1).to.deep.equal(['Expected type TestEnum.']); const result2 = coerceValue({ field: 'value' }, TestEnum); - expectError(result2, 'Expected type TestEnum.'); + expectErrors(result2).to.deep.equal(['Expected type TestEnum.']); }); }); @@ -184,28 +178,26 @@ describe('coerceValue', () => { it('returns no error for a valid input', () => { const result = coerceValue({ foo: 123 }, TestInputObject); - expectNoErrors(result); - expect(result.value).to.deep.equal({ foo: 123 }); + expectValue(result).to.deep.equal({ foo: 123 }); }); it('returns no error for a non-object type', () => { const result = coerceValue(123, TestInputObject); - expectError(result, 'Expected type TestInputObject to be an object.'); + expectErrors(result).to.deep.equal([ + 'Expected type TestInputObject to be an object.', + ]); }); it('returns no error for an invalid field', () => { const result = coerceValue({ foo: 'abc' }, TestInputObject); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Expected type Int at value.foo; Int cannot represent non 32-bit signed integer value: abc', - ); + ]); }); it('returns multiple errors for multiple invalid fields', () => { const result = coerceValue({ foo: 'abc', bar: 'def' }, TestInputObject); - expect( - result.errors && result.errors.map(error => error.message), - ).to.deep.equal([ + expectErrors(result).to.deep.equal([ 'Expected type Int at value.foo; Int cannot represent non 32-bit signed integer value: abc', 'Expected type Int at value.bar; Int cannot represent non 32-bit signed integer value: def', ]); @@ -213,10 +205,9 @@ describe('coerceValue', () => { it('returns error for a missing required field', () => { const result = coerceValue({ bar: 123 }, TestInputObject); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Field value.foo of required type Int! was not provided.', - ); + ]); }); it('returns error for an unknown field', () => { @@ -224,18 +215,16 @@ describe('coerceValue', () => { { foo: 123, unknownField: 123 }, TestInputObject, ); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Field "unknownField" is not defined by type TestInputObject.', - ); + ]); }); it('returns error for a misspelled field', () => { const result = coerceValue({ foo: 123, bart: 123 }, TestInputObject); - expectError( - result, + expectErrors(result).to.deep.equal([ 'Field "bart" is not defined by type TestInputObject; did you mean bar?', - ); + ]); }); }); });