diff options
| -rw-r--r-- | .tool-versions | 2 | ||||
| -rw-r--r-- | src/lib/components/tag/tag.svelte | 4 | ||||
| -rw-r--r-- | src/lib/data/queries.ts | 90 | ||||
| -rw-r--r-- | src/lib/stores/apollo.ts | 62 | ||||
| -rw-r--r-- | src/lib/stores/posts.test.ts | 12 | ||||
| -rw-r--r-- | src/lib/stores/tags.test.ts | 295 |
6 files changed, 456 insertions, 9 deletions
diff --git a/.tool-versions b/.tool-versions index 826ccf4..a851188 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 16.15.0 +nodejs 20.4.0 diff --git a/src/lib/components/tag/tag.svelte b/src/lib/components/tag/tag.svelte index 1744094..71e8eff 100644 --- a/src/lib/components/tag/tag.svelte +++ b/src/lib/components/tag/tag.svelte @@ -1,7 +1,7 @@ <script lang="ts"> - import type { Tag, Topic } from '$lib/data/types'; + import type { Topic } from '$lib/data/types'; - export let tag: Tag; + export let tag: string; export let topics: Topic[]; import { _ } from 'svelte-i18n'; diff --git a/src/lib/data/queries.ts b/src/lib/data/queries.ts new file mode 100644 index 0000000..7364c0f --- /dev/null +++ b/src/lib/data/queries.ts @@ -0,0 +1,90 @@ +import { gql } from '@apollo/client/core'; + +export const GET_FORUMS = gql` + query GetForums { + forums { + id + glyph + label + position + } + } +`; + +export const GET_FORUM = gql` + query GetForum($id: ID!) { + forum(id: $id) { + id + glyph + label + position + topics { + id + title + updated_at + ttl + } + } + } +`; + +export const GET_TAG = gql` + query GetTag($id: ID!) { + tag(id: $id) { + id + topics { + id + title + updated_at + ttl + } + } + } +`; + +export const GET_TOPIC = gql` + query GetTopic($id: ID!) { + topic(id: $id) { + id + title + updated_at + ttl + forum { + id + glyph + label + } + tags { + id + weight + } + posts { + id + text + created_at + author { + id + handle + } + } + } + } +`; + +export const GET_POST = gql` + query GetPost($id: ID!) { + post(id: $id) { + id + text + created_at + author { + id + handle + } + topic { + id + title + } + } + } +`; diff --git a/src/lib/stores/apollo.ts b/src/lib/stores/apollo.ts new file mode 100644 index 0000000..4ef1986 --- /dev/null +++ b/src/lib/stores/apollo.ts @@ -0,0 +1,62 @@ +import { ApolloError } from '@apollo/client/core'; +import { readable } from 'svelte/store'; +import { client } from '$lib/config/apollo'; +import type { DocumentNode, ApolloQueryResult } from '@apollo/client/core'; + +import type { Readable } from 'svelte/store'; + +/* + * This is a generic store for use with apollo + */ + +type ApolloStoreConfiguration<Type> = { + key: string; + query: DocumentNode; + initialValue?: Type | void; + variables?: object; +}; + +type ApolloStoreState<Type> = { + loading: boolean; + data: Type | void; + error: Error | void; +}; + +export const store = function store<Type>({ + key, + query, + initialValue = null, + variables = {} +}: ApolloStoreConfiguration<Type>): Readable<ApolloStoreState<Type>> { + const initialState: ApolloStoreState<Type> = { + loading: true, + data: initialValue, + error: undefined + }; + + return readable(initialState, (set) => { + const handleError = function (error: Error) { + return set({ + loading: false, + data: initialValue, + error + }); + }; + + client.watchQuery({ query, variables }).subscribe( + (result: ApolloQueryResult<Type>) => { + if (result.errors) { + const error = new ApolloError({ graphQLErrors: result.errors }); + return handleError(error); + } + + set({ + loading: false, + data: result.data[key], + error: undefined + }); + }, + (error: Error) => handleError(error) + ); + }); +}; diff --git a/src/lib/stores/posts.test.ts b/src/lib/stores/posts.test.ts index ef1835d..df9b57c 100644 --- a/src/lib/stores/posts.test.ts +++ b/src/lib/stores/posts.test.ts @@ -7,13 +7,13 @@ const { eachLike, like } = Matchers; jest.mock('$lib/config/config.ts'); -import { getPost } from './posts'; +import { post } from './posts'; const internals = { provider: null }; -describe('Posts store pact', () => { +describe('post', () => { beforeAll(async () => { internals.provider = new Pact({ port: 1234, @@ -90,7 +90,7 @@ describe('Posts store pact', () => { }); test('it returns the post', async () => { - const post = getPost('8f75eba5-6989-4dd3-b466-e464546ce374'); + const post = post('8f75eba5-6989-4dd3-b466-e464546ce374'); const { counter, promise: resolveAfterTwo } = resolveAfter(2); let response = null; post.subscribe((postValue) => { @@ -169,7 +169,7 @@ describe('Posts store pact', () => { }); test('it returns the post', async () => { - const post = getPost('8f75eba5-6989-4dd3-b466-e464546ce374'); + const post = post('8f75eba5-6989-4dd3-b466-e464546ce374'); const { counter, promise: resolveAfterTwo } = resolveAfter(2); let response = null; post.subscribe((postValue) => { @@ -228,7 +228,7 @@ describe('Posts store pact', () => { }); test('it returns the error', async () => { - const post = getPost('8f75eba5-6989-4dd3-b466-e464546ce374'); + const post = post('8f75eba5-6989-4dd3-b466-e464546ce374'); const { counter, promise: resolveAfterTwo } = resolveAfter(2); let response = null; post.subscribe((postValue) => { @@ -295,7 +295,7 @@ describe('Posts store pact', () => { }); test('it returns the error', async () => { - const post = getPost('8f75eba5-6989-4dd3-b466-e464546ce374'); + const post = post('8f75eba5-6989-4dd3-b466-e464546ce374'); const { counter, promise: resolveAfterTwo } = resolveAfter(2); let response = null; post.subscribe((postValue) => { diff --git a/src/lib/stores/tags.test.ts b/src/lib/stores/tags.test.ts new file mode 100644 index 0000000..8c9effe --- /dev/null +++ b/src/lib/stores/tags.test.ts @@ -0,0 +1,295 @@ +import { GraphQLInteraction, Pact, Matchers } from '@pact-foundation/pact'; +import { resolve } from 'path'; + +import { resolveAfter } from '$lib/utils/resolve_after'; + +const { eachLike, like } = Matchers; + +jest.mock('$lib/config/config.ts'); + +import { getTag } from './tags'; + +const internals = { + provider: null +}; + +describe('Tags store pact', () => { + beforeAll(async () => { + internals.provider = new Pact({ + port: 1234, + dir: resolve(process.cwd(), 'pacts'), + consumer: 'ForumClient', + provider: 'ForumServer', + pactfileWriteMode: 'update' + }); + + await internals.provider.setup(); + }); + + afterEach(() => internals.provider.verify()); + afterAll(() => internals.provider.finalize()); + + describe("When there's data", () => { + describe('GetTag', () => { + beforeAll(async () => { + const tagQuery = new GraphQLInteraction() + .given("there's data") + .uponReceiving('a request to get a single tag') + .withRequest({ + path: '/graphql', + method: 'POST' + }) + .withOperation('GetTag') + .withQuery( + `query GetTag($id: ID!) { + tag(id: $id) { + id + topics { + id + title + updated_at + ttl + __typename + } + __typename + } + }` + ) + .withVariables({ + id: 'pineapple' + }) + .willRespondWith({ + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: { + data: { + tag: { + id: like('pineapple'), + topics: eachLike({ + id: like('cd038ae7-e8b4-4e38-9543-3d697e69ac34'), + title: like('This topic is about pineapples'), + updated_at: like(1619978944077), + ttl: like(3555) + }) + } + } + } + }); + return await internals.provider.addInteraction(tagQuery); + }); + + test('it returns the tag', async () => { + const tag = getTag('pineapple'); + const { counter, promise: resolveAfterTwo } = resolveAfter(2); + let response = null; + tag.subscribe((tagValue) => { + response = tagValue; + counter(); + }); + expect(response.data).toBe(null); + expect(response.loading).toBe(true); + expect(response.error).toBe(undefined); + await resolveAfterTwo; + expect(response.data).toEqual({ + id: 'pineapple', + topics: [ + { + id: 'cd038ae7-e8b4-4e38-9543-3d697e69ac34', + title: 'This topic is about pineapples', + updated_at: 1619978944077, + ttl: 3555 + } + ] + }); + expect(response.loading).toBe(false); + expect(response.error).toBe(undefined); + }); + }); + }); + + describe("When there's no data", () => { + describe('GetTag', () => { + beforeAll(async () => { + const tagQuery = new GraphQLInteraction() + .given("there's no data") + .uponReceiving('a request to get a single tag') + .withRequest({ + path: '/graphql', + method: 'POST' + }) + .withOperation('GetTag') + .withQuery( + `query GetTag($id: ID!) { + tag(id: $id) { + id + topics { + id + title + updated_at + ttl + __typename + } + __typename + } + }` + ) + .withVariables({ + id: 'pineapple' + }) + .willRespondWith({ + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: { + data: { + tag: null + } + } + }); + return await internals.provider.addInteraction(tagQuery); + }); + + test('it returns the tag', async () => { + const tag = getTag('pineapple'); + const { counter, promise: resolveAfterTwo } = resolveAfter(2); + let response = null; + tag.subscribe((tagValue) => { + response = tagValue; + counter(); + }); + expect(response.data).toBe(null); + expect(response.loading).toBe(true); + expect(response.error).toBe(undefined); + await resolveAfterTwo; + expect(response.data).toBe(null); + expect(response.loading).toBe(false); + expect(response.error).toBe(undefined); + }); + }); + }); + + describe("When there's a server error", () => { + describe('GetTag', () => { + beforeAll(async () => { + const tagQuery = new GraphQLInteraction() + .given("there's a server error") + .uponReceiving('a request to get a single tag') + .withRequest({ + path: '/graphql', + method: 'POST' + }) + .withOperation('GetTag') + .withQuery( + `query GetTag($id: ID!) { + tag(id: $id) { + id + topics { + id + title + updated_at + ttl + __typename + } + __typename + } + }` + ) + .withVariables({ + id: 'pineapple' + }) + .willRespondWith({ + status: 500 + }); + return await internals.provider.addInteraction(tagQuery); + }); + + test('it returns the error', async () => { + const tag = getTag('pineapple'); + const { counter, promise: resolveAfterTwo } = resolveAfter(2); + let response = null; + tag.subscribe((tagValue) => { + response = tagValue; + counter(); + }); + expect(response.data).toBe(null); + expect(response.loading).toBe(true); + expect(response.error).toBe(undefined); + await resolveAfterTwo; + expect(response.data).toBe(null); + expect(response.loading).toBe(false); + expect(response.error).toBeInstanceOf(Error); + }); + }); + }); + + describe("When there's an error in the response", () => { + describe('GetTag', () => { + beforeAll(async () => { + const tagQuery = new GraphQLInteraction() + .given("there's an error in the response") + .uponReceiving('a request to get a single tag') + .withRequest({ + path: '/graphql', + method: 'POST' + }) + .withOperation('GetTag') + .withQuery( + `query GetTag($id: ID!) { + tag(id: $id) { + id + topics { + id + title + updated_at + ttl + __typename + } + __typename + } + }` + ) + .withVariables({ + id: 'pineapple' + }) + .willRespondWith({ + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: { + errors: eachLike({ + message: like('An error occurred when fetching the tag') + }) + } + }); + return await internals.provider.addInteraction(tagQuery); + }); + + test('it returns the error', async () => { + const tag = getTag('pineapple'); + const { counter, promise: resolveAfterTwo } = resolveAfter(2); + let response = null; + tag.subscribe((tagValue) => { + response = tagValue; + counter(); + }); + expect(response.data).toBe(null); + expect(response.loading).toBe(true); + expect(response.error).toBe(undefined); + await resolveAfterTwo; + expect(response.data).toBe(null); + expect(response.loading).toBe(false); + expect(response.error.graphQLErrors).toEqual( + expect.arrayContaining([ + { + message: 'An error occurred when fetching the tag' + } + ]) + ); + }); + }); + }); +}); |