From 1d0b83d88b5dd6a824587e90229aeb78cba772a6 Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Thu, 11 Jan 2018 17:24:04 -0600 Subject: [PATCH 1/2] feat: add tests --- src/connector.js | 2 +- src/mocks.js | 3 +- src/model.js | 8 ++-- src/resolvers.js | 4 +- src/schema.graphql | 18 ++++---- test/connector.test.js | 14 +++++++ test/context.test.js | 13 ------ test/index.test.js | 4 +- test/mocks.test.js | 16 +++---- test/model.test.js | 95 ++++++++++++++++++++++++++++++++++++++++++ test/resolvers.test.js | 54 ++++++++++++++++++------ 11 files changed, 177 insertions(+), 54 deletions(-) create mode 100644 test/connector.test.js delete mode 100644 test/context.test.js create mode 100644 test/model.test.js diff --git a/src/connector.js b/src/connector.js index 3bc78f0..965d5d4 100644 --- a/src/connector.js +++ b/src/connector.js @@ -7,4 +7,4 @@ export default class NumbersConnector extends GraphQLConnector { this.headers['Content-Type'] = 'application/json'; this.apiBaseUri = `http://numbersapi.com`; } -} \ No newline at end of file +} diff --git a/src/mocks.js b/src/mocks.js index cdf03dd..99842f3 100644 --- a/src/mocks.js +++ b/src/mocks.js @@ -1,4 +1,3 @@ -import { MockList } from 'graphql-tools'; import casual from 'casual'; export default { @@ -8,6 +7,6 @@ export default { found: casual.boolean, number: casual.number, type: casual.random_element(['trivia', 'date', 'math', 'year']), - date: casual.date('mmm D') + date: casual.date('mmm D'), }), }; diff --git a/src/model.js b/src/model.js index 02dbfda..8ee58af 100644 --- a/src/model.js +++ b/src/model.js @@ -2,15 +2,13 @@ import { GrampsError } from '@gramps/errors'; import { GraphQLModel } from '@gramps/rest-helpers'; export default class NumbersModel extends GraphQLModel { - async get(number, type) { - console.log(await this.connector.get(`/${number}/${type}`)) return this.connector.get(`/${number}/${type}`).catch(res => this.throwError(res.error, { description: 'Could not get the info', }), ); - } + } /** * Throws a custom GrAMPS error. @@ -35,6 +33,6 @@ export default class NumbersModel extends GraphQLModel { docsLink: 'https://ibm.biz/gramps-data-source-tutorial', }; - throw GrampsError(Object.assign({defaults}, {customErrorData})); + throw GrampsError(Object.assign({ defaults }, { customErrorData })); } -} \ No newline at end of file +} diff --git a/src/resolvers.js b/src/resolvers.js index 988f8c0..eb7eb02 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -1,10 +1,8 @@ export default { Query: { - // TODO: Update query resolver name and args to match the schema - // TODO: Update the context method to load the correct data trivia: (_, { number }, context) => context.get(number, 'trivia'), date: (_, { date }, context) => context.get(date, 'date'), math: (_, { number }, context) => context.get(number, 'math'), year: (_, { number }, context) => context.get(number, 'year'), - } + }, }; diff --git a/src/schema.graphql b/src/schema.graphql index c003a91..5489902 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,14 +1,14 @@ type Query { - trivia(number: Int): Numbers_Trivia - date(date: String): Numbers_Trivia - math(number: Int): Numbers_Trivia - year(number: Int): Numbers_Trivia + trivia(number: Int!): Numbers_Trivia! + date(date: String!): Numbers_Trivia! + math(number: Int!): Numbers_Trivia! + year(number: Int!): Numbers_Trivia! } type Numbers_Trivia { - text: String - found: Boolean - number: Int - type: String - date: String + text: String! + found: Boolean! + number: Int! + type: String! + date: String! } diff --git a/test/connector.test.js b/test/connector.test.js new file mode 100644 index 0000000..9a60603 --- /dev/null +++ b/test/connector.test.js @@ -0,0 +1,14 @@ +import { GraphQLConnector } from '@gramps/rest-helpers'; +import Connector from '../src/connector'; + +const connector = new Connector(); + +describe(`NumbersConnector`, () => { + it('inherits the GraphQLConnector class', () => { + expect(connector).toBeInstanceOf(GraphQLConnector); + }); + + it('uses the appropriate URL', () => { + expect(connector.apiBaseUri).toBe(`http://numbersapi.com`); + }); +}); diff --git a/test/context.test.js b/test/context.test.js deleted file mode 100644 index 672ed60..0000000 --- a/test/context.test.js +++ /dev/null @@ -1,13 +0,0 @@ -import context from '../src/context'; - -describe('Data Source Context', () => { - describe('getById()', () => { - it('loads data by its ID', () => { - expect(context.getById(123)).toEqual({ - id: 123, - name: 'GrAMPS GraphQL Data Source Base', - lucky_numbers: [1, 2, 3, 5, 8, 13, 21], - }); - }); - }); -}); diff --git a/test/index.test.js b/test/index.test.js index ee77a8e..0474075 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,9 +1,9 @@ import dataSource from '../src'; // TODO: Update the data source name. -describe(`Data Source: DataSourceBase`, () => { +describe(`Data Source: Numbers`, () => { it('contains a namespace property', () => { - expect(dataSource.namespace).toBe('DataSourceBase'); + expect(dataSource.namespace).toBe('Numbers'); }); it('contains a context property', () => { diff --git a/test/mocks.test.js b/test/mocks.test.js index 5a2bf9a..f84a63d 100644 --- a/test/mocks.test.js +++ b/test/mocks.test.js @@ -1,15 +1,17 @@ import expectMockFields from './helpers/expectMockFields'; -import expectMockList from './helpers/expectMockList'; import mocks from '../src/mocks'; describe('mock resolvers', () => { - describe('PFX_DataSourceBase', () => { - const mockResolvers = mocks.PFX_DataSourceBase(); + describe('Numbers_Trivia', () => { + const mockResolvers = mocks.Numbers_Trivia(); // This helper creates a test to ensure each field has a mock resolver. - expectMockFields(mockResolvers, ['id', 'name', 'lucky_numbers']); - - // This helper creates a test to check that these fields return `MockList`s. - expectMockList(mockResolvers, ['lucky_numbers']); + expectMockFields(mockResolvers, [ + 'text', + 'found', + 'number', + 'type', + 'date', + ]); }); }); diff --git a/test/model.test.js b/test/model.test.js new file mode 100644 index 0000000..8358aba --- /dev/null +++ b/test/model.test.js @@ -0,0 +1,95 @@ +import { GraphQLModel } from '@gramps/rest-helpers'; +import Model from '../src/model'; +import Connector from '../src/connector'; + +// Mock the connector because we’re only testing the model here. +jest.mock('../src/connector', () => + jest.fn(() => ({ + get: jest.fn(() => Promise.resolve()), + apiBaseUri: 'https://example.org', + })), +); + +const DATA_SOURCE_NAME = 'Numbers'; + +const connector = new Connector(); +const model = new Model({ connector }); + +describe(`${DATA_SOURCE_NAME}Model`, () => { + it('inherits the GraphQLModel class', () => { + expect(model).toBeInstanceOf(GraphQLModel); + }); + + describe('get()', () => { + it('calls the correct endpoint to load the number trivia', () => { + const spy = jest.spyOn(connector, 'get'); + + model.get(1234, 'trivia'); + expect(spy).toHaveBeenCalledWith('/1234/trivia'); + }); + + it('throws a GrampsError if something goes wrong', async () => { + expect.assertions(1); + + model.connector.get.mockImplementationOnce(() => + Promise.reject(Error('boom')), + ); + + try { + await model.get(1, 'trivia'); + } catch (error) { + expect(error.isBoom).toEqual(true); + } + }); + }); + + describe.skip('throwError()', () => { + const mockError = { + statusCode: 401, + }; + + it('converts an error from the endpoint into a GrampsError', async () => { + expect.assertions(4); + + /* + * To simulate a failed call, we tell Jest to return a rejected Promise + * with our mock error. + */ + model.connector.get.mockImplementationOnce(() => + Promise.reject(mockError), + ); + + try { + await model.get(1234, 'trivia'); + } catch (error) { + // Check that GrampsError properly received the error detail. + expect(error).toHaveProperty('isBoom', true); + expect(error.output).toHaveProperty('statusCode', 401); + expect(error.output.payload).toHaveProperty( + 'targetEndpoint', + 'https://example.org/1234/info.0.json', + ); + expect(error.output.payload).toHaveProperty( + 'graphqlModel', + `${DATA_SOURCE_NAME}Model`, + ); + } + }); + + it('creates a default GrampsError if no custom error data is supplied', async () => { + try { + await model.throwError({}); + } catch (error) { + expect(error.output.statusCode).toBe(500); + expect(error.output.payload.errorCode).toBe( + `${DATA_SOURCE_NAME}Model_Error`, + ); + expect(error.output.payload.description).toBe('Something went wrong.'); + expect(error.output.payload.graphqlModel).toBe( + `${DATA_SOURCE_NAME}Model`, + ); + expect(error.output.payload.targetEndpoint).toBeNull(); + } + }); + }); +}); diff --git a/test/resolvers.test.js b/test/resolvers.test.js index 2e862e2..66c58d5 100644 --- a/test/resolvers.test.js +++ b/test/resolvers.test.js @@ -2,25 +2,55 @@ import resolvers from '../src/resolvers'; import expectNullable from './helpers/expectNullable'; describe('Data Source Resolvers', () => { - describe('query resolvers', () => { - describe('getById()', () => { - it('loads a thing by its ID', () => { + describe('Query', () => { + const mockContext = { + get: (val, type) => Promise.resolve({ [type]: val }), + }; + + describe('trivia', () => { + test('loads trivia for a number', () => { expect.assertions(1); - const mockContext = { - getById: id => Promise.resolve(id), - }; + return expect( + resolvers.Query.trivia({}, { number: 123 }, mockContext), + ).resolves.toEqual({ trivia: 123 }); + }); + }); + + describe('date', () => { + test('loads trivia for a date', () => { + expect.assertions(1); return expect( - resolvers.Query.getById({}, { id: 123 }, mockContext), - ).resolves.toEqual(123); + resolvers.Query.date({}, { date: '2017-08' }, mockContext), + ).resolves.toEqual({ date: '2017-08' }); }); }); - }); - describe('PFX_DataSourceBase', () => { - const resolver = resolvers.PFX_DataSourceBase; + describe('math', () => { + test('loads math-related trivia for a number', () => { + expect.assertions(1); - expectNullable(resolver, ['name']); + return expect( + resolvers.Query.math({}, { number: 123 }, mockContext), + ).resolves.toEqual({ math: 123 }); + }); + }); + + describe('year', () => { + test('loads year-related trivia for a number', () => { + expect.assertions(1); + + return expect( + resolvers.Query.year({}, { number: 123 }, mockContext), + ).resolves.toEqual({ year: 123 }); + }); + }); }); + + // describe('PFX_DataSourceBase', () => { + // const resolver = resolvers.PFX_DataSourceBase; + + // expectNullable(resolver, ['name']); + // }); }); From f378fdfb100847e888207839668d3dbc4cb7c5d9 Mon Sep 17 00:00:00 2001 From: Jason Lengstorf Date: Sun, 14 Jan 2018 09:50:06 -0600 Subject: [PATCH 2/2] feat: return object from context - move context Model into an object to avoid `this` problems - make `date` and `year` fields optional - add docs for the `date` query and the `Numbers_Trivia` type - remove references to internal IBM docs --- package.json | 2 +- src/index.js | 2 +- src/model.js | 2 +- src/resolvers.js | 12 ++++++++---- src/schema.graphql | 22 +++++++++++++++++++++- test/resolvers.test.js | 12 +++++++----- 6 files changed, 39 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 25b406f..98aa914 100644 --- a/package.json +++ b/package.json @@ -84,5 +84,5 @@ } } }, - "version": "0.0.0-development" + "version": "1.0.0-beta1" } diff --git a/src/index.js b/src/index.js index e06b7ea..d82aadf 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,7 @@ import mocks from './mocks'; */ export default { namespace: 'Numbers', - context: new Model({ connector: new Connector() }), + context: { model: new Model({ connector: new Connector() }) }, typeDefs, resolvers, mocks, diff --git a/src/model.js b/src/model.js index 8ee58af..48a483b 100644 --- a/src/model.js +++ b/src/model.js @@ -30,7 +30,7 @@ export default class NumbersModel extends GraphQLModel { errorCode: `${this.constructor.name}_Error`, description: message, graphqlModel: this.constructor.name, - docsLink: 'https://ibm.biz/gramps-data-source-tutorial', + docsLink: 'https://gramps.js.org/data-source/data-source-overview/', }; throw GrampsError(Object.assign({ defaults }, { customErrorData })); diff --git a/src/resolvers.js b/src/resolvers.js index eb7eb02..5b0a241 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -1,8 +1,12 @@ export default { Query: { - trivia: (_, { number }, context) => context.get(number, 'trivia'), - date: (_, { date }, context) => context.get(date, 'date'), - math: (_, { number }, context) => context.get(number, 'math'), - year: (_, { number }, context) => context.get(number, 'year'), + trivia: (_, { number }, context) => context.model.get(number, 'trivia'), + date: (_, { date }, context) => context.model.get(date, 'date'), + math: (_, { number }, context) => context.model.get(number, 'math'), + year: (_, { number }, context) => context.model.get(number, 'year'), + }, + Numbers_Trivia: { + date: data => data.date || null, + year: data => data.year || null, }, }; diff --git a/src/schema.graphql b/src/schema.graphql index 5489902..c253bde 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,14 +1,34 @@ type Query { trivia(number: Int!): Numbers_Trivia! + + """ + Load trivia for a data using the `MM/DD` format. + + For example, to load trivia for January 14th: + + `date(date: "1/14") { text }` + """ date(date: String!): Numbers_Trivia! math(number: Int!): Numbers_Trivia! year(number: Int!): Numbers_Trivia! } type Numbers_Trivia { + "A string of the fact text itself." text: String! + + "Boolean of whether there was a fact for the requested number." found: Boolean! + + "The number that was queried." number: Int! + + "String of the category of the returned fact." type: String! - date: String! + + "If relevant, a day of year associated with some year facts, as a string." + date: String + + "If relevant, a year associated with some date facts, as a string." + year: String } diff --git a/test/resolvers.test.js b/test/resolvers.test.js index 66c58d5..46f58cb 100644 --- a/test/resolvers.test.js +++ b/test/resolvers.test.js @@ -4,7 +4,9 @@ import expectNullable from './helpers/expectNullable'; describe('Data Source Resolvers', () => { describe('Query', () => { const mockContext = { - get: (val, type) => Promise.resolve({ [type]: val }), + model: { + get: (val, type) => Promise.resolve({ [type]: val }), + }, }; describe('trivia', () => { @@ -48,9 +50,9 @@ describe('Data Source Resolvers', () => { }); }); - // describe('PFX_DataSourceBase', () => { - // const resolver = resolvers.PFX_DataSourceBase; + describe('Numbers_Trivia', () => { + const resolver = resolvers.Numbers_Trivia; - // expectNullable(resolver, ['name']); - // }); + expectNullable(resolver, ['date', 'year']); + }); });