From 50642c5fcd9a6cc0ad31be685f706c6def4e0ce2 Mon Sep 17 00:00:00 2001 From: JohnAlbin Date: Wed, 21 Feb 2024 17:17:47 +0800 Subject: [PATCH] test(next-drupal): reorganize Jest tests Fixes #608 --- .editorconfig | 3 + packages/next-drupal/jest.config.cjs | 9 +- packages/next-drupal/src/client.ts | 83 +- packages/next-drupal/src/draft.ts | 1 + packages/next-drupal/src/types/resource.ts | 3 +- packages/next-drupal/tests/.eslintrc.json | 11 + .../__snapshots__/basic-methods.test.ts.snap | 406 +++ .../fetch-related-methods.test.ts.snap} | 2355 ++------------- .../pages-router-methods.test.ts.snap | 2255 ++++++++++++++ .../tests/DrupalClient/basic-methods.test.ts | 597 ++++ .../tests/DrupalClient/constructor.test.ts | 333 +++ .../crud-methods.test.ts} | 160 +- .../fetch-related-methods.test.ts | 1186 ++++++++ .../DrupalClient/getters-setters.test.ts | 224 ++ .../DrupalClient/pages-router-methods.test.ts | 1252 ++++++++ .../next-drupal/tests/Logger/logger.test.ts | 69 + packages/next-drupal/tests/__mocks__/next.ts | 37 + .../tests/__mocks__/next/headers.ts | 86 + packages/next-drupal/tests/client.test.ts | 2647 ----------------- .../next-drupal/tests/draft/draft.test.ts | 206 ++ packages/next-drupal/tests/utils/index.ts | 2 + .../next-drupal/tests/utils/mocks/data.ts | 365 +++ .../next-drupal/tests/utils/mocks/fetch.ts | 63 + .../next-drupal/tests/utils/mocks/index.ts | 3 + .../next-drupal/tests/utils/mocks/logger.ts | 10 + .../tests/{utils.ts => utils/rpc.ts} | 7 +- 26 files changed, 7619 insertions(+), 4754 deletions(-) create mode 100644 packages/next-drupal/tests/.eslintrc.json create mode 100644 packages/next-drupal/tests/DrupalClient/__snapshots__/basic-methods.test.ts.snap rename packages/next-drupal/tests/{__snapshots__/client.test.ts.snap => DrupalClient/__snapshots__/fetch-related-methods.test.ts.snap} (81%) create mode 100644 packages/next-drupal/tests/DrupalClient/__snapshots__/pages-router-methods.test.ts.snap create mode 100644 packages/next-drupal/tests/DrupalClient/basic-methods.test.ts create mode 100644 packages/next-drupal/tests/DrupalClient/constructor.test.ts rename packages/next-drupal/tests/{crud.test.ts => DrupalClient/crud-methods.test.ts} (67%) create mode 100644 packages/next-drupal/tests/DrupalClient/fetch-related-methods.test.ts create mode 100644 packages/next-drupal/tests/DrupalClient/getters-setters.test.ts create mode 100644 packages/next-drupal/tests/DrupalClient/pages-router-methods.test.ts create mode 100644 packages/next-drupal/tests/Logger/logger.test.ts create mode 100644 packages/next-drupal/tests/__mocks__/next.ts create mode 100644 packages/next-drupal/tests/__mocks__/next/headers.ts delete mode 100644 packages/next-drupal/tests/client.test.ts create mode 100644 packages/next-drupal/tests/draft/draft.test.ts create mode 100644 packages/next-drupal/tests/utils/index.ts create mode 100644 packages/next-drupal/tests/utils/mocks/data.ts create mode 100644 packages/next-drupal/tests/utils/mocks/fetch.ts create mode 100644 packages/next-drupal/tests/utils/mocks/index.ts create mode 100644 packages/next-drupal/tests/utils/mocks/logger.ts rename packages/next-drupal/tests/{utils.ts => utils/rpc.ts} (76%) diff --git a/.editorconfig b/.editorconfig index ae10a5cc..a090db43 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,6 @@ indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true + +[{*.diff,*.patch}] +trim_trailing_whitespace = false diff --git a/packages/next-drupal/jest.config.cjs b/packages/next-drupal/jest.config.cjs index dbf98e5f..a7ccdfe7 100644 --- a/packages/next-drupal/jest.config.cjs +++ b/packages/next-drupal/jest.config.cjs @@ -13,6 +13,7 @@ module.exports = { }, ], }, + testLocationInResults: true, coverageProvider: "v8", collectCoverage: true, collectCoverageFrom: ["./src/**"], @@ -25,10 +26,10 @@ module.exports = { coverageReporters: ["lcov", "text", "text-summary"], coverageThreshold: { global: { - statements: 82.07, - branches: 86.9, - functions: 80.76, - lines: 82.07, + statements: 100, + branches: 100, + functions: 100, + lines: 100, }, }, } diff --git a/packages/next-drupal/src/client.ts b/packages/next-drupal/src/client.ts index ed383140..0e51bc01 100644 --- a/packages/next-drupal/src/client.ts +++ b/packages/next-drupal/src/client.ts @@ -264,7 +264,7 @@ export class DrupalClient { if (token) { init["headers"]["Authorization"] = `Bearer ${token.access_token}` } - } else if (isAccessTokenAuth(this._auth)) { + } /* c8 ignore next 4 */ else if (isAccessTokenAuth(this._auth)) { init["headers"]["Authorization"] = `${this._auth.token_type} ${this._auth.access_token}` } @@ -273,7 +273,7 @@ export class DrupalClient { this.debug(`Using custom authorization header.`) init["headers"]["Authorization"] = init.withAuth - } else if (typeof init.withAuth === "function") { + } /* c8 ignore next 4 */ else if (typeof init.withAuth === "function") { this.debug(`Using custom authorization callback.`) init["headers"]["Authorization"] = init.withAuth() @@ -292,7 +292,7 @@ export class DrupalClient { if (token) { init["headers"]["Authorization"] = `Bearer ${token.access_token}` } - } else if (isAccessTokenAuth(init.withAuth)) { + } /* c8 ignore next 4 */ else if (isAccessTokenAuth(init.withAuth)) { init["headers"]["Authorization"] = `${init.withAuth.token_type} ${init.withAuth.access_token}` } @@ -322,7 +322,9 @@ export class DrupalClient { const apiPath = await this.getEntryForResourceType( type, - options?.locale !== options?.defaultLocale ? options.locale : undefined + options?.locale !== options?.defaultLocale + ? /* c8 ignore next */ options.locale + : undefined ) const url = this.buildUrl(apiPath, options?.params) @@ -338,13 +340,13 @@ export class DrupalClient { withAuth: options.withAuth, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) const json = await response.json() - return options.deserialize ? this.deserialize(json) : json + return options.deserialize + ? this.deserialize(json) + : /* c8 ignore next */ json } async createFileResource( @@ -383,9 +385,7 @@ export class DrupalClient { withAuth: options.withAuth, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) const json = await response.json() @@ -406,7 +406,9 @@ export class DrupalClient { const apiPath = await this.getEntryForResourceType( type, - options?.locale !== options?.defaultLocale ? options.locale : undefined + options?.locale !== options?.defaultLocale + ? /* c8 ignore next */ options.locale + : undefined ) const url = this.buildUrl(`${apiPath}/${uuid}`, options?.params) @@ -423,13 +425,13 @@ export class DrupalClient { withAuth: options.withAuth, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) const json = await response.json() - return options.deserialize ? this.deserialize(json) : json + return options.deserialize + ? this.deserialize(json) + : /* c8 ignore next */ json } async deleteResource( @@ -445,7 +447,9 @@ export class DrupalClient { const apiPath = await this.getEntryForResourceType( type, - options?.locale !== options?.defaultLocale ? options.locale : undefined + options?.locale !== options?.defaultLocale + ? /* c8 ignore next */ options.locale + : undefined ) const url = this.buildUrl(`${apiPath}/${uuid}`, options?.params) @@ -457,9 +461,7 @@ export class DrupalClient { withAuth: options.withAuth, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) return response.status === 204 } @@ -477,6 +479,7 @@ export class DrupalClient { ...options, } + /* c8 ignore next 11 */ if (options.withCache) { const cached = (await this.cache.get(options.cacheKey)) as string @@ -502,12 +505,11 @@ export class DrupalClient { withAuth: options.withAuth, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) const json = await response.json() + /* c8 ignore next 3 */ if (options.withCache) { await this.cache.set(options.cacheKey, JSON.stringify(json)) } @@ -568,6 +570,7 @@ export class DrupalClient { // When we try to translate /es/example, decoupled router will properly // translate to the untranslated version and set the locale to es. // However a subrequests to /es/subrequests for decoupled router will fail. + /* c8 ignore next 3 */ if (context.locale && input.entity.langcode !== context.locale) { context.locale = input.entity.langcode } @@ -621,7 +624,7 @@ export class DrupalClient { options.defaultLocale && path.indexOf(options.locale) !== 1 ) { - path = path === "/" ? path : path.replace(/^\/+/, "") + path = path === "/" ? /* c8 ignore next */ path : path.replace(/^\/+/, "") path = this.getPathFromContext({ params: { slug: [path] }, locale: options.locale, @@ -735,9 +738,7 @@ export class DrupalClient { withAuth: options.withAuth, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) const json = await response.json() @@ -1028,6 +1029,7 @@ export class DrupalClient { const pattern = `^\\/${locale}\\/` const path = href.replace(this.baseUrl, "") + /* c8 ignore next 3 */ if (!new RegExp(pattern, "i").test(path)) { return `${this.baseUrl}/${locale}${path}` } @@ -1073,6 +1075,7 @@ export class DrupalClient { response: NextApiResponse, options?: Parameters[0] ) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { slug, resourceVersion, plugin, secret, scope, ...draftData } = request.query const useDraftMode = options?.enable @@ -1167,6 +1170,7 @@ export class DrupalClient { ...options, } + /* c8 ignore next 9 */ if (options.withCache) { const cached = (await this.cache.get(options.cacheKey)) as string @@ -1192,13 +1196,13 @@ export class DrupalClient { withAuth: options.withAuth, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) const data = await response.json() - const items = options.deserialize ? this.deserialize(data) : data + const items = options.deserialize + ? this.deserialize(data) + : /* c8 ignore next */ data const { items: tree } = this.buildMenuTree(items) @@ -1207,6 +1211,7 @@ export class DrupalClient { tree, } + /* c8 ignore next 3 */ if (options.withCache) { await this.cache.set(options.cacheKey, JSON.stringify(menu)) } @@ -1265,9 +1270,7 @@ export class DrupalClient { withAuth: options.withAuth, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) const data = await response.json() @@ -1307,9 +1310,7 @@ export class DrupalClient { withAuth: options.withAuth, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) const json = await response.json() @@ -1374,7 +1375,7 @@ export class DrupalClient { const clientId = opts?.clientId || this._auth.clientId const clientSecret = opts?.clientSecret || this._auth.clientSecret - const url = this.buildUrl(opts?.url || this._auth.url || DEFAULT_AUTH_URL) + const url = this.buildUrl(opts?.url || this._auth.url) if ( this.accessTokenScope === opts?.scope && @@ -1407,9 +1408,7 @@ export class DrupalClient { body, }) - if (!response?.ok) { - await this.handleJsonApiErrors(response) - } + await this.throwIfJsonApiErrors(response) const result: AccessToken = await response.json() @@ -1476,7 +1475,7 @@ export class DrupalClient { throw error } - private async handleJsonApiErrors(response: Response) { + private async throwIfJsonApiErrors(response: Response) { if (!response?.ok) { const errors = await this.getErrorsFromResponse(response) throw new JsonApiErrors(errors, response.status) diff --git a/packages/next-drupal/src/draft.ts b/packages/next-drupal/src/draft.ts index 32f42cd0..a598db72 100644 --- a/packages/next-drupal/src/draft.ts +++ b/packages/next-drupal/src/draft.ts @@ -34,6 +34,7 @@ export async function enableDraftMode( } // Send Drupal's data to the draft-mode page. + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { secret, scope, plugin, ...draftData } = Object.fromEntries( searchParams.entries() ) diff --git a/packages/next-drupal/src/types/resource.ts b/packages/next-drupal/src/types/resource.ts index a6aa12be..99e5f28b 100644 --- a/packages/next-drupal/src/types/resource.ts +++ b/packages/next-drupal/src/types/resource.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import type { JsonApiError, JsonApiLinks } from "../jsonapi-errors" import type { PathAlias } from "./drupal" @@ -53,7 +55,6 @@ export interface JsonApiUpdateResourceBody { } } -/* eslint-disable @typescript-eslint/no-explicit-any */ export interface JsonApiResource extends Record { id: string type: string diff --git a/packages/next-drupal/tests/.eslintrc.json b/packages/next-drupal/tests/.eslintrc.json new file mode 100644 index 00000000..6da4ba50 --- /dev/null +++ b/packages/next-drupal/tests/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "extends": ["../../../.eslintrc.json"], + "overrides": [ + { + "files": ["*.test.ts"], + "rules": { + "@typescript-eslint/ban-ts-comment": "off" + } + } + ] +} diff --git a/packages/next-drupal/tests/DrupalClient/__snapshots__/basic-methods.test.ts.snap b/packages/next-drupal/tests/DrupalClient/__snapshots__/basic-methods.test.ts.snap new file mode 100644 index 00000000..c1cac65e --- /dev/null +++ b/packages/next-drupal/tests/DrupalClient/__snapshots__/basic-methods.test.ts.snap @@ -0,0 +1,406 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`deserialize() allows for custom data serializer 1`] = ` +{ + "id": "52837ad0-f218-46bd-a106-5710336b7053", + "title": "TITLE: Give it a go and grow your own herbs", +} +`; + +exports[`deserialize() deserializes JSON:API collection 1`] = ` +[ + { + "id": "52837ad0-f218-46bd-a106-5710336b7053", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", + }, + }, + "title": "Give it a go and grow your own herbs", + "type": "node--article", + }, + { + "id": "3d5a7bca-5b4a-49ee-87d2-cbe313562903", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/3d5a7bca-5b4a-49ee-87d2-cbe313562903?resourceVersion=id%3A22", + }, + }, + "title": "Dairy-free and delicious milk chocolate", + "type": "node--article", + }, + { + "id": "a1ef61c9-ed15-4b6d-bba2-bf6e5141f961", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/a1ef61c9-ed15-4b6d-bba2-bf6e5141f961?resourceVersion=id%3A24", + }, + }, + "title": "The real deal for supermarket savvy shopping", + "type": "node--article", + }, + { + "id": "ac0b8b56-db8d-4322-9b1e-224f22083f0d", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ac0b8b56-db8d-4322-9b1e-224f22083f0d?resourceVersion=id%3A26", + }, + }, + "title": "The Umami guide to our favorite mushrooms", + "type": "node--article", + }, + { + "id": "22874ecc-0443-441b-a3c9-3aa94d85b800", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/22874ecc-0443-441b-a3c9-3aa94d85b800?resourceVersion=id%3A28", + }, + }, + "title": "Let's hear it for carrots", + "type": "node--article", + }, + { + "id": "ec51b1de-f51e-4761-b061-aa2624e58b4a", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ec51b1de-f51e-4761-b061-aa2624e58b4a?resourceVersion=id%3A30", + }, + }, + "title": "Baking mishaps - our troubleshooting tips", + "type": "node--article", + }, + { + "id": "d5b9ef80-c0aa-45ca-88e7-79c28abe5d50", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/d5b9ef80-c0aa-45ca-88e7-79c28abe5d50?resourceVersion=id%3A32", + }, + }, + "title": "Skip the spirits with delicious mocktails", + "type": "node--article", + }, + { + "id": "6c7c249f-cd1a-41d5-944b-b3bfce68b325", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/6c7c249f-cd1a-41d5-944b-b3bfce68b325?resourceVersion=id%3A34", + }, + }, + "title": "Give your oatmeal the ultimate makeover", + "type": "node--article", + }, +] +`; + +exports[`deserialize() deserializes JSON:API resource 1`] = ` +{ + "body": { + "format": "basic_html", + "processed": "

There's nothing like having your own supply of fresh herbs, readily available and close at hand to use while cooking. Whether you have a large garden or a small kitchen window sill, there's always enough room for something home grown.

+

Outdoors

+

Mint

+

Mint is a great plant to grow as it's hardy and can grow in almost any soil. Mint can grow wild, so keep it contained in a pot or it might spread and take over your whole garden.

+

Sage

+

Like mint, sage is another prolific growing plant and will take over your garden if you let it. Highly aromatic, the sage plant can be planted in a pot or flower bed in well drained soil. The best way to store the herb is to sun dry the leaves and store in a cool, dark cupboard in a sealed container.

+

Rosemary

+

Rosemary plants grow into lovely shrubs. Easily grown from cuttings, rosemary plants do not like freezing temperatures so keep pots or planted bushes near the home to shelter them from the cold. It grows well in pots as it likes dry soil, but can survive well in the ground too. If pruning rosemary to encourage it into a better shape, save the branches and hang them upside down to preserve the flavor and use in food.

+

Indoors

+

Basil

+

Perfect in sunny spot on a kitchen window sill. Basil is an annual plant, so will die off in the autumn, so it's a good idea to harvest it in the summer if you have an abundance and dry it. Picked basil stays fresh longer if it is placed in water (like fresh flowers). A great way to store basil is to make it into pesto!

+

Chives

+

A versatile herb, chives can grow well indoors. Ensure the plant is watered well, and gets plenty of light. Remember to regularly trim the chives. This prevents the flowers from developing and encourages new growth.

+

Coriander (Cilantro)

+

Coriander can grow indoors, but unlike the other herbs, it doesn't like full sun. If you have a south facing kitchen window, this isn't the place for it. Although not as thirsty as basil, coriander doesn't like dry soil so don't forget to water it! Cut coriander is best stored in the fridge.

+", + "summary": null, + "value": "

There's nothing like having your own supply of fresh herbs, readily available and close at hand to use while cooking. Whether you have a large garden or a small kitchen window sill, there's always enough room for something home grown.

+

Outdoors

+

Mint

+

Mint is a great plant to grow as it's hardy and can grow in almost any soil. Mint can grow wild, so keep it contained in a pot or it might spread and take over your whole garden.

+

Sage

+

Like mint, sage is another prolific growing plant and will take over your garden if you let it. Highly aromatic, the sage plant can be planted in a pot or flower bed in well drained soil. The best way to store the herb is to sun dry the leaves and store in a cool, dark cupboard in a sealed container.

+

Rosemary

+

Rosemary plants grow into lovely shrubs. Easily grown from cuttings, rosemary plants do not like freezing temperatures so keep pots or planted bushes near the home to shelter them from the cold. It grows well in pots as it likes dry soil, but can survive well in the ground too. If pruning rosemary to encourage it into a better shape, save the branches and hang them upside down to preserve the flavor and use in food.

+

Indoors

+

Basil

+

Perfect in sunny spot on a kitchen window sill. Basil is an annual plant, so will die off in the autumn, so it's a good idea to harvest it in the summer if you have an abundance and dry it. Picked basil stays fresh longer if it is placed in water (like fresh flowers). A great way to store basil is to make it into pesto!

+

Chives

+

A versatile herb, chives can grow well indoors. Ensure the plant is watered well, and gets plenty of light. Remember to regularly trim the chives. This prevents the flowers from developing and encourages new growth.

+

Coriander (Cilantro)

+

Coriander can grow indoors, but unlike the other herbs, it doesn't like full sun. If you have a south facing kitchen window, this isn't the place for it. Although not as thirsty as basil, coriander doesn't like dry soil so don't forget to water it! Cut coriander is best stored in the fridge.

+", + }, + "changed": "2022-03-21T10:52:42+00:00", + "content_translation_outdated": false, + "content_translation_source": "und", + "created": "2022-03-21T10:52:42+00:00", + "default_langcode": true, + "drupal_internal__nid": 10, + "drupal_internal__vid": 20, + "field_media_image": { + "id": "e5091a16-134e-400d-8393-cfe4eccbcaa2", + "resourceIdObjMeta": { + "drupal_internal__target_id": 10, + }, + "type": "media--image", + }, + "field_tags": [ + { + "changed": "2022-03-21T10:52:42+00:00", + "content_translation_created": "2022-03-21T10:52:42+00:00", + "content_translation_outdated": false, + "content_translation_source": "und", + "content_translation_uid": { + "id": "256a133b-0bd7-4426-a823-b8ce81e0d778", + "resourceIdObjMeta": { + "drupal_internal__target_id": 0, + }, + "type": "user--user", + }, + "default_langcode": true, + "description": null, + "drupal_internal__revision_id": 14, + "drupal_internal__tid": 14, + "id": "dcd81647-71b7-48cb-b555-e20322bcb7a7", + "langcode": "en", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/taxonomy_term/tags/dcd81647-71b7-48cb-b555-e20322bcb7a7?resourceVersion=id%3A14", + }, + }, + "name": "Grow your own", + "parent": [ + { + "id": "virtual", + "resourceIdObjMeta": { + "links": { + "help": { + "href": "https://www.drupal.org/docs/8/modules/json-api/core-concepts#virtual", + "meta": { + "about": "Usage and meaning of the 'virtual' resource identifier.", + }, + }, + }, + }, + "type": "taxonomy_term--tags", + }, + ], + "path": { + "alias": "/tags/grow-your-own", + "langcode": "en", + "pid": 27, + }, + "relationshipNames": [ + "vid", + "revision_user", + "parent", + "content_translation_uid", + ], + "resourceIdObjMeta": { + "drupal_internal__target_id": 14, + }, + "revision_created": "2022-03-21T10:52:42+00:00", + "revision_log_message": null, + "revision_translation_affected": true, + "revision_user": null, + "status": true, + "type": "taxonomy_term--tags", + "vid": { + "id": "b0442737-e5ff-4c7c-8c2b-c977886d6e73", + "resourceIdObjMeta": { + "drupal_internal__target_id": "tags", + }, + "type": "taxonomy_vocabulary--taxonomy_vocabulary", + }, + "weight": 0, + }, + { + "changed": "2022-03-21T10:52:42+00:00", + "content_translation_created": "2022-03-21T10:52:42+00:00", + "content_translation_outdated": false, + "content_translation_source": "und", + "content_translation_uid": { + "id": "256a133b-0bd7-4426-a823-b8ce81e0d778", + "resourceIdObjMeta": { + "drupal_internal__target_id": 0, + }, + "type": "user--user", + }, + "default_langcode": true, + "description": null, + "drupal_internal__revision_id": 23, + "drupal_internal__tid": 23, + "id": "60d20a4c-9d42-4b25-b717-3af3cba6abe8", + "langcode": "en", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/taxonomy_term/tags/60d20a4c-9d42-4b25-b717-3af3cba6abe8?resourceVersion=id%3A23", + }, + }, + "name": "Seasonal", + "parent": [ + { + "id": "virtual", + "resourceIdObjMeta": { + "links": { + "help": { + "href": "https://www.drupal.org/docs/8/modules/json-api/core-concepts#virtual", + "meta": { + "about": "Usage and meaning of the 'virtual' resource identifier.", + }, + }, + }, + }, + "type": "taxonomy_term--tags", + }, + ], + "path": { + "alias": "/tags/seasonal", + "langcode": "en", + "pid": 45, + }, + "relationshipNames": [ + "vid", + "revision_user", + "parent", + "content_translation_uid", + ], + "resourceIdObjMeta": { + "drupal_internal__target_id": 23, + }, + "revision_created": "2022-03-21T10:52:42+00:00", + "revision_log_message": null, + "revision_translation_affected": true, + "revision_user": null, + "status": true, + "type": "taxonomy_term--tags", + "vid": { + "id": "b0442737-e5ff-4c7c-8c2b-c977886d6e73", + "resourceIdObjMeta": { + "drupal_internal__target_id": "tags", + }, + "type": "taxonomy_vocabulary--taxonomy_vocabulary", + }, + "weight": 0, + }, + { + "changed": "2022-03-21T10:52:42+00:00", + "content_translation_created": "2022-03-21T10:52:42+00:00", + "content_translation_outdated": false, + "content_translation_source": "und", + "content_translation_uid": { + "id": "256a133b-0bd7-4426-a823-b8ce81e0d778", + "resourceIdObjMeta": { + "drupal_internal__target_id": 0, + }, + "type": "user--user", + }, + "default_langcode": true, + "description": null, + "drupal_internal__revision_id": 16, + "drupal_internal__tid": 16, + "id": "57a1d9f6-23a6-4215-a8a9-582202cd938d", + "langcode": "en", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/taxonomy_term/tags/57a1d9f6-23a6-4215-a8a9-582202cd938d?resourceVersion=id%3A16", + }, + }, + "name": "Herbs", + "parent": [ + { + "id": "virtual", + "resourceIdObjMeta": { + "links": { + "help": { + "href": "https://www.drupal.org/docs/8/modules/json-api/core-concepts#virtual", + "meta": { + "about": "Usage and meaning of the 'virtual' resource identifier.", + }, + }, + }, + }, + "type": "taxonomy_term--tags", + }, + ], + "path": { + "alias": "/tags/herbs", + "langcode": "en", + "pid": 31, + }, + "relationshipNames": [ + "vid", + "revision_user", + "parent", + "content_translation_uid", + ], + "resourceIdObjMeta": { + "drupal_internal__target_id": 16, + }, + "revision_created": "2022-03-21T10:52:42+00:00", + "revision_log_message": null, + "revision_translation_affected": true, + "revision_user": null, + "status": true, + "type": "taxonomy_term--tags", + "vid": { + "id": "b0442737-e5ff-4c7c-8c2b-c977886d6e73", + "resourceIdObjMeta": { + "drupal_internal__target_id": "tags", + }, + "type": "taxonomy_vocabulary--taxonomy_vocabulary", + }, + "weight": 0, + }, + ], + "id": "52837ad0-f218-46bd-a106-5710336b7053", + "langcode": "en", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", + }, + }, + "moderation_state": "published", + "node_type": { + "id": "a145b65a-e660-4f5d-ac0d-bd2ff9e3f0b0", + "resourceIdObjMeta": { + "drupal_internal__target_id": "article", + }, + "type": "node_type--node_type", + }, + "path": { + "alias": "/articles/give-it-a-go-and-grow-your-own-herbs", + "langcode": "en", + "pid": 85, + }, + "promote": true, + "relationshipNames": [ + "node_type", + "revision_uid", + "uid", + "field_media_image", + "field_tags", + ], + "revision_log": null, + "revision_timestamp": "2022-03-21T10:52:42+00:00", + "revision_translation_affected": null, + "revision_uid": { + "id": "dd9c916d-4d66-4bff-a851-eeba0cf7673a", + "resourceIdObjMeta": { + "drupal_internal__target_id": 5, + }, + "type": "user--user", + }, + "status": true, + "sticky": false, + "title": "Give it a go and grow your own herbs", + "type": "node--article", + "uid": { + "id": "dd9c916d-4d66-4bff-a851-eeba0cf7673a", + "resourceIdObjMeta": { + "drupal_internal__target_id": 5, + }, + "type": "user--user", + }, +} +`; diff --git a/packages/next-drupal/tests/__snapshots__/client.test.ts.snap b/packages/next-drupal/tests/DrupalClient/__snapshots__/fetch-related-methods.test.ts.snap similarity index 81% rename from packages/next-drupal/tests/__snapshots__/client.test.ts.snap rename to packages/next-drupal/tests/DrupalClient/__snapshots__/fetch-related-methods.test.ts.snap index ae2b97f0..120346a7 100644 --- a/packages/next-drupal/tests/__snapshots__/client.test.ts.snap +++ b/packages/next-drupal/tests/DrupalClient/__snapshots__/fetch-related-methods.test.ts.snap @@ -1,287 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`buildStaticPathsFromResources it builds static paths from resources 1`] = ` -[ - { - "params": { - "slug": [ - "blog", - "post", - "one", - ], - }, - }, - { - "params": { - "slug": [ - "blog", - "post", - "two", - ], - }, - }, -] -`; - -exports[`buildStaticPathsFromResources it builds static paths from resources 2`] = ` -[ - { - "locale": "es", - "params": { - "slug": [ - "blog", - "post", - "one", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "blog", - "post", - "two", - ], - }, - }, -] -`; - -exports[`buildStaticPathsFromResources it builds static paths from resources with pathPrefix 1`] = ` -[ - { - "params": { - "slug": [ - "post", - "one", - ], - }, - }, - { - "params": { - "slug": [ - "post", - "two", - ], - }, - }, -] -`; - -exports[`buildStaticPathsFromResources it builds static paths from resources with pathPrefix 2`] = ` -[ - { - "locale": "es", - "params": { - "slug": [ - "one", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "two", - ], - }, - }, -] -`; - -exports[`buildStaticPathsParamsFromPaths it builds static paths from paths 1`] = ` -[ - { - "params": { - "slug": [ - "blog", - "post", - "one", - ], - }, - }, - { - "params": { - "slug": [ - "blog", - "post", - "two", - ], - }, - }, - { - "params": { - "slug": [ - "blog", - "post", - "three", - ], - }, - }, -] -`; - -exports[`buildStaticPathsParamsFromPaths it builds static paths from paths 2`] = ` -[ - { - "locale": "en", - "params": { - "slug": [ - "blog", - "post", - "one", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "blog", - "post", - "two", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "blog", - "post", - "three", - ], - }, - }, -] -`; - -exports[`buildStaticPathsParamsFromPaths it builds static paths from paths with pathPrefix 1`] = ` -[ - { - "params": { - "slug": [ - "post", - "one", - ], - }, - }, - { - "params": { - "slug": [ - "post", - "two", - ], - }, - }, - { - "params": { - "slug": [ - "post", - ], - }, - }, -] -`; - -exports[`deserialize it allows for custom data serializer 1`] = ` -{ - "id": "52837ad0-f218-46bd-a106-5710336b7053", - "title": "TITLE: Give it a go and grow your own herbs", -} -`; - -exports[`deserialize it deserializes JSON:API collection 1`] = ` -[ - { - "id": "52837ad0-f218-46bd-a106-5710336b7053", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", - }, - }, - "title": "Give it a go and grow your own herbs", - "type": "node--article", - }, - { - "id": "3d5a7bca-5b4a-49ee-87d2-cbe313562903", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/3d5a7bca-5b4a-49ee-87d2-cbe313562903?resourceVersion=id%3A22", - }, - }, - "title": "Dairy-free and delicious milk chocolate", - "type": "node--article", - }, - { - "id": "a1ef61c9-ed15-4b6d-bba2-bf6e5141f961", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/a1ef61c9-ed15-4b6d-bba2-bf6e5141f961?resourceVersion=id%3A24", - }, - }, - "title": "The real deal for supermarket savvy shopping", - "type": "node--article", - }, - { - "id": "ac0b8b56-db8d-4322-9b1e-224f22083f0d", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ac0b8b56-db8d-4322-9b1e-224f22083f0d?resourceVersion=id%3A26", - }, - }, - "title": "The Umami guide to our favorite mushrooms", - "type": "node--article", - }, - { - "id": "22874ecc-0443-441b-a3c9-3aa94d85b800", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/22874ecc-0443-441b-a3c9-3aa94d85b800?resourceVersion=id%3A28", - }, - }, - "title": "Let's hear it for carrots", - "type": "node--article", - }, - { - "id": "ec51b1de-f51e-4761-b061-aa2624e58b4a", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ec51b1de-f51e-4761-b061-aa2624e58b4a?resourceVersion=id%3A30", - }, - }, - "title": "Baking mishaps - our troubleshooting tips", - "type": "node--article", - }, - { - "id": "d5b9ef80-c0aa-45ca-88e7-79c28abe5d50", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/d5b9ef80-c0aa-45ca-88e7-79c28abe5d50?resourceVersion=id%3A32", - }, - }, - "title": "Skip the spirits with delicious mocktails", - "type": "node--article", - }, - { - "id": "6c7c249f-cd1a-41d5-944b-b3bfce68b325", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/6c7c249f-cd1a-41d5-944b-b3bfce68b325?resourceVersion=id%3A34", - }, - }, - "title": "Give your oatmeal the ultimate makeover", - "type": "node--article", - }, -] -`; - -exports[`deserialize it deserializes JSON:API resource 1`] = ` +exports[`fetch() allows fetching custom url 1`] = ` { - "body": { - "format": "basic_html", - "processed": "

There's nothing like having your own supply of fresh herbs, readily available and close at hand to use while cooking. Whether you have a large garden or a small kitchen window sill, there's always enough room for something home grown.

+ "data": { + "attributes": { + "body": { + "format": "basic_html", + "processed": "

There's nothing like having your own supply of fresh herbs, readily available and close at hand to use while cooking. Whether you have a large garden or a small kitchen window sill, there's always enough room for something home grown.

Outdoors

Mint

Mint is a great plant to grow as it's hardy and can grow in almost any soil. Mint can grow wild, so keep it contained in a pot or it might spread and take over your whole garden.

@@ -297,8 +22,8 @@ exports[`deserialize it deserializes JSON:API resource 1`] = `

Coriander (Cilantro)

Coriander can grow indoors, but unlike the other herbs, it doesn't like full sun. If you have a south facing kitchen window, this isn't the place for it. Although not as thirsty as basil, coriander doesn't like dry soil so don't forget to water it! Cut coriander is best stored in the fridge.

", - "summary": null, - "value": "

There's nothing like having your own supply of fresh herbs, readily available and close at hand to use while cooking. Whether you have a large garden or a small kitchen window sill, there's always enough room for something home grown.

+ "summary": null, + "value": "

There's nothing like having your own supply of fresh herbs, readily available and close at hand to use while cooking. Whether you have a large garden or a small kitchen window sill, there's always enough room for something home grown.

Outdoors

Mint

Mint is a great plant to grow as it's hardy and can grow in almost any soil. Mint can grow wild, so keep it contained in a pot or it might spread and take over your whole garden.

@@ -314,348 +39,33 @@ exports[`deserialize it deserializes JSON:API resource 1`] = `

Coriander (Cilantro)

Coriander can grow indoors, but unlike the other herbs, it doesn't like full sun. If you have a south facing kitchen window, this isn't the place for it. Although not as thirsty as basil, coriander doesn't like dry soil so don't forget to water it! Cut coriander is best stored in the fridge.

", - }, - "changed": "2022-03-21T10:52:42+00:00", - "content_translation_outdated": false, - "content_translation_source": "und", - "created": "2022-03-21T10:52:42+00:00", - "default_langcode": true, - "drupal_internal__nid": 10, - "drupal_internal__vid": 20, - "field_media_image": { - "id": "e5091a16-134e-400d-8393-cfe4eccbcaa2", - "resourceIdObjMeta": { - "drupal_internal__target_id": 10, - }, - "type": "media--image", - }, - "field_tags": [ - { + }, "changed": "2022-03-21T10:52:42+00:00", - "content_translation_created": "2022-03-21T10:52:42+00:00", "content_translation_outdated": false, "content_translation_source": "und", - "content_translation_uid": { - "id": "256a133b-0bd7-4426-a823-b8ce81e0d778", - "resourceIdObjMeta": { - "drupal_internal__target_id": 0, - }, - "type": "user--user", - }, + "created": "2022-03-21T10:52:42+00:00", "default_langcode": true, - "description": null, - "drupal_internal__revision_id": 14, - "drupal_internal__tid": 14, - "id": "dcd81647-71b7-48cb-b555-e20322bcb7a7", + "drupal_internal__nid": 10, + "drupal_internal__vid": 20, "langcode": "en", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/taxonomy_term/tags/dcd81647-71b7-48cb-b555-e20322bcb7a7?resourceVersion=id%3A14", - }, - }, - "name": "Grow your own", - "parent": [ - { - "id": "virtual", - "resourceIdObjMeta": { - "links": { - "help": { - "href": "https://www.drupal.org/docs/8/modules/json-api/core-concepts#virtual", - "meta": { - "about": "Usage and meaning of the 'virtual' resource identifier.", - }, - }, - }, - }, - "type": "taxonomy_term--tags", - }, - ], + "moderation_state": "published", "path": { - "alias": "/tags/grow-your-own", + "alias": "/articles/give-it-a-go-and-grow-your-own-herbs", "langcode": "en", - "pid": 27, - }, - "relationshipNames": [ - "vid", - "revision_user", - "parent", - "content_translation_uid", - ], - "resourceIdObjMeta": { - "drupal_internal__target_id": 14, + "pid": 85, }, - "revision_created": "2022-03-21T10:52:42+00:00", - "revision_log_message": null, - "revision_translation_affected": true, - "revision_user": null, + "promote": true, + "revision_log": null, + "revision_timestamp": "2022-03-21T10:52:42+00:00", + "revision_translation_affected": null, "status": true, - "type": "taxonomy_term--tags", - "vid": { - "id": "b0442737-e5ff-4c7c-8c2b-c977886d6e73", - "resourceIdObjMeta": { - "drupal_internal__target_id": "tags", - }, - "type": "taxonomy_vocabulary--taxonomy_vocabulary", - }, - "weight": 0, + "sticky": false, + "title": "Give it a go and grow your own herbs", }, - { - "changed": "2022-03-21T10:52:42+00:00", - "content_translation_created": "2022-03-21T10:52:42+00:00", - "content_translation_outdated": false, - "content_translation_source": "und", - "content_translation_uid": { - "id": "256a133b-0bd7-4426-a823-b8ce81e0d778", - "resourceIdObjMeta": { - "drupal_internal__target_id": 0, - }, - "type": "user--user", - }, - "default_langcode": true, - "description": null, - "drupal_internal__revision_id": 23, - "drupal_internal__tid": 23, - "id": "60d20a4c-9d42-4b25-b717-3af3cba6abe8", - "langcode": "en", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/taxonomy_term/tags/60d20a4c-9d42-4b25-b717-3af3cba6abe8?resourceVersion=id%3A23", - }, - }, - "name": "Seasonal", - "parent": [ - { - "id": "virtual", - "resourceIdObjMeta": { - "links": { - "help": { - "href": "https://www.drupal.org/docs/8/modules/json-api/core-concepts#virtual", - "meta": { - "about": "Usage and meaning of the 'virtual' resource identifier.", - }, - }, - }, - }, - "type": "taxonomy_term--tags", - }, - ], - "path": { - "alias": "/tags/seasonal", - "langcode": "en", - "pid": 45, - }, - "relationshipNames": [ - "vid", - "revision_user", - "parent", - "content_translation_uid", - ], - "resourceIdObjMeta": { - "drupal_internal__target_id": 23, - }, - "revision_created": "2022-03-21T10:52:42+00:00", - "revision_log_message": null, - "revision_translation_affected": true, - "revision_user": null, - "status": true, - "type": "taxonomy_term--tags", - "vid": { - "id": "b0442737-e5ff-4c7c-8c2b-c977886d6e73", - "resourceIdObjMeta": { - "drupal_internal__target_id": "tags", - }, - "type": "taxonomy_vocabulary--taxonomy_vocabulary", - }, - "weight": 0, - }, - { - "changed": "2022-03-21T10:52:42+00:00", - "content_translation_created": "2022-03-21T10:52:42+00:00", - "content_translation_outdated": false, - "content_translation_source": "und", - "content_translation_uid": { - "id": "256a133b-0bd7-4426-a823-b8ce81e0d778", - "resourceIdObjMeta": { - "drupal_internal__target_id": 0, - }, - "type": "user--user", - }, - "default_langcode": true, - "description": null, - "drupal_internal__revision_id": 16, - "drupal_internal__tid": 16, - "id": "57a1d9f6-23a6-4215-a8a9-582202cd938d", - "langcode": "en", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/taxonomy_term/tags/57a1d9f6-23a6-4215-a8a9-582202cd938d?resourceVersion=id%3A16", - }, - }, - "name": "Herbs", - "parent": [ - { - "id": "virtual", - "resourceIdObjMeta": { - "links": { - "help": { - "href": "https://www.drupal.org/docs/8/modules/json-api/core-concepts#virtual", - "meta": { - "about": "Usage and meaning of the 'virtual' resource identifier.", - }, - }, - }, - }, - "type": "taxonomy_term--tags", - }, - ], - "path": { - "alias": "/tags/herbs", - "langcode": "en", - "pid": 31, - }, - "relationshipNames": [ - "vid", - "revision_user", - "parent", - "content_translation_uid", - ], - "resourceIdObjMeta": { - "drupal_internal__target_id": 16, - }, - "revision_created": "2022-03-21T10:52:42+00:00", - "revision_log_message": null, - "revision_translation_affected": true, - "revision_user": null, - "status": true, - "type": "taxonomy_term--tags", - "vid": { - "id": "b0442737-e5ff-4c7c-8c2b-c977886d6e73", - "resourceIdObjMeta": { - "drupal_internal__target_id": "tags", - }, - "type": "taxonomy_vocabulary--taxonomy_vocabulary", - }, - "weight": 0, - }, - ], - "id": "52837ad0-f218-46bd-a106-5710336b7053", - "langcode": "en", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", - }, - }, - "moderation_state": "published", - "node_type": { - "id": "a145b65a-e660-4f5d-ac0d-bd2ff9e3f0b0", - "resourceIdObjMeta": { - "drupal_internal__target_id": "article", - }, - "type": "node_type--node_type", - }, - "path": { - "alias": "/articles/give-it-a-go-and-grow-your-own-herbs", - "langcode": "en", - "pid": 85, - }, - "promote": true, - "relationshipNames": [ - "node_type", - "revision_uid", - "uid", - "field_media_image", - "field_tags", - ], - "revision_log": null, - "revision_timestamp": "2022-03-21T10:52:42+00:00", - "revision_translation_affected": null, - "revision_uid": { - "id": "dd9c916d-4d66-4bff-a851-eeba0cf7673a", - "resourceIdObjMeta": { - "drupal_internal__target_id": 5, - }, - "type": "user--user", - }, - "status": true, - "sticky": false, - "title": "Give it a go and grow your own herbs", - "type": "node--article", - "uid": { - "id": "dd9c916d-4d66-4bff-a851-eeba0cf7673a", - "resourceIdObjMeta": { - "drupal_internal__target_id": 5, - }, - "type": "user--user", - }, -} -`; - -exports[`fetch it allows fetching custom url 1`] = ` -{ - "data": { - "attributes": { - "body": { - "format": "basic_html", - "processed": "

There's nothing like having your own supply of fresh herbs, readily available and close at hand to use while cooking. Whether you have a large garden or a small kitchen window sill, there's always enough room for something home grown.

-

Outdoors

-

Mint

-

Mint is a great plant to grow as it's hardy and can grow in almost any soil. Mint can grow wild, so keep it contained in a pot or it might spread and take over your whole garden.

-

Sage

-

Like mint, sage is another prolific growing plant and will take over your garden if you let it. Highly aromatic, the sage plant can be planted in a pot or flower bed in well drained soil. The best way to store the herb is to sun dry the leaves and store in a cool, dark cupboard in a sealed container.

-

Rosemary

-

Rosemary plants grow into lovely shrubs. Easily grown from cuttings, rosemary plants do not like freezing temperatures so keep pots or planted bushes near the home to shelter them from the cold. It grows well in pots as it likes dry soil, but can survive well in the ground too. If pruning rosemary to encourage it into a better shape, save the branches and hang them upside down to preserve the flavor and use in food.

-

Indoors

-

Basil

-

Perfect in sunny spot on a kitchen window sill. Basil is an annual plant, so will die off in the autumn, so it's a good idea to harvest it in the summer if you have an abundance and dry it. Picked basil stays fresh longer if it is placed in water (like fresh flowers). A great way to store basil is to make it into pesto!

-

Chives

-

A versatile herb, chives can grow well indoors. Ensure the plant is watered well, and gets plenty of light. Remember to regularly trim the chives. This prevents the flowers from developing and encourages new growth.

-

Coriander (Cilantro)

-

Coriander can grow indoors, but unlike the other herbs, it doesn't like full sun. If you have a south facing kitchen window, this isn't the place for it. Although not as thirsty as basil, coriander doesn't like dry soil so don't forget to water it! Cut coriander is best stored in the fridge.

-", - "summary": null, - "value": "

There's nothing like having your own supply of fresh herbs, readily available and close at hand to use while cooking. Whether you have a large garden or a small kitchen window sill, there's always enough room for something home grown.

-

Outdoors

-

Mint

-

Mint is a great plant to grow as it's hardy and can grow in almost any soil. Mint can grow wild, so keep it contained in a pot or it might spread and take over your whole garden.

-

Sage

-

Like mint, sage is another prolific growing plant and will take over your garden if you let it. Highly aromatic, the sage plant can be planted in a pot or flower bed in well drained soil. The best way to store the herb is to sun dry the leaves and store in a cool, dark cupboard in a sealed container.

-

Rosemary

-

Rosemary plants grow into lovely shrubs. Easily grown from cuttings, rosemary plants do not like freezing temperatures so keep pots or planted bushes near the home to shelter them from the cold. It grows well in pots as it likes dry soil, but can survive well in the ground too. If pruning rosemary to encourage it into a better shape, save the branches and hang them upside down to preserve the flavor and use in food.

-

Indoors

-

Basil

-

Perfect in sunny spot on a kitchen window sill. Basil is an annual plant, so will die off in the autumn, so it's a good idea to harvest it in the summer if you have an abundance and dry it. Picked basil stays fresh longer if it is placed in water (like fresh flowers). A great way to store basil is to make it into pesto!

-

Chives

-

A versatile herb, chives can grow well indoors. Ensure the plant is watered well, and gets plenty of light. Remember to regularly trim the chives. This prevents the flowers from developing and encourages new growth.

-

Coriander (Cilantro)

-

Coriander can grow indoors, but unlike the other herbs, it doesn't like full sun. If you have a south facing kitchen window, this isn't the place for it. Although not as thirsty as basil, coriander doesn't like dry soil so don't forget to water it! Cut coriander is best stored in the fridge.

-", - }, - "changed": "2022-03-21T10:52:42+00:00", - "content_translation_outdated": false, - "content_translation_source": "und", - "created": "2022-03-21T10:52:42+00:00", - "default_langcode": true, - "drupal_internal__nid": 10, - "drupal_internal__vid": 20, - "langcode": "en", - "moderation_state": "published", - "path": { - "alias": "/articles/give-it-a-go-and-grow-your-own-herbs", - "langcode": "en", - "pid": 85, - }, - "promote": true, - "revision_log": null, - "revision_timestamp": "2022-03-21T10:52:42+00:00", - "revision_translation_affected": null, - "status": true, - "sticky": false, - "title": "Give it a go and grow your own herbs", - }, - "id": "52837ad0-f218-46bd-a106-5710336b7053", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", + "id": "52837ad0-f218-46bd-a106-5710336b7053", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", }, }, "relationships": { @@ -781,7 +191,7 @@ exports[`fetch it allows fetching custom url 1`] = ` } `; -exports[`getIndex it fetches the JSON:API index 1`] = ` +exports[`getIndex() fetches the JSON:API index 1`] = ` { "data": [], "jsonapi": { @@ -991,7 +401,7 @@ exports[`getIndex it fetches the JSON:API index 1`] = ` } `; -exports[`getIndex it fetches the JSON:API index with locale 1`] = ` +exports[`getIndex() fetches the JSON:API index with locale 1`] = ` { "data": [], "jsonapi": { @@ -1201,7 +611,7 @@ exports[`getIndex it fetches the JSON:API index with locale 1`] = ` } `; -exports[`getMenu it fetches menu items for a menu 1`] = ` +exports[`getMenu() fetches menu items for a menu 1`] = ` { "items": [ { @@ -1336,7 +746,7 @@ exports[`getMenu it fetches menu items for a menu 1`] = ` } `; -exports[`getMenu it fetches menu items for a menu with locale 1`] = ` +exports[`getMenu() fetches menu items for a menu with locale 1`] = ` { "items": [ { @@ -1471,7 +881,7 @@ exports[`getMenu it fetches menu items for a menu with locale 1`] = ` } `; -exports[`getMenu it fetches menu items for a menu with params 1`] = ` +exports[`getMenu() fetches menu items for a menu with params 1`] = ` { "items": [ { @@ -1494,36 +904,7 @@ exports[`getMenu it fetches menu items for a menu with params 1`] = ` } `; -exports[`getPathFromContext it encodes path with punctuation 1`] = ` -{ - "entity": { - "bundle": "page", - "canonical": "https://tests.next-drupal.org/en/path%26with%5Epunc%26in%24path", - "id": "20", - "langcode": "en", - "path": "/en/path%26with%5Epunc%26in%24path", - "type": "node", - "uuid": "e4bbe727-14ee-44bc-a34a-14ce5c86a82e", - }, - "isHomePath": false, - "jsonapi": { - "basePath": "/en/jsonapi", - "entryPoint": "https://tests.next-drupal.org/en/jsonapi", - "individual": "https://tests.next-drupal.org/en/jsonapi/node/page/e4bbe727-14ee-44bc-a34a-14ce5c86a82e", - "pathPrefix": "en/jsonapi", - "resourceName": "node--page", - }, - "label": "Page with characters in path", - "meta": { - "deprecated": { - "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.", - }, - }, - "resolved": "https://tests.next-drupal.org/en/path%26with%5Epunc%26in%24path", -} -`; - -exports[`getResource it fetches a resource by uuid 1`] = ` +exports[`getResource() fetches a resource by uuid 1`] = ` { "changed": "2022-03-25T08:02:17+00:00", "content_translation_outdated": false, @@ -1660,7 +1041,7 @@ exports[`getResource it fetches a resource by uuid 1`] = ` } `; -exports[`getResource it fetches a resource by uuid with params 1`] = ` +exports[`getResource() fetches a resource by uuid with params 1`] = ` { "field_cooking_time": 30, "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", @@ -1674,7 +1055,7 @@ exports[`getResource it fetches a resource by uuid with params 1`] = ` } `; -exports[`getResource it fetches a resource using locale 1`] = ` +exports[`getResource() fetches a resource using locale 1`] = ` { "field_cooking_time": 30, "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", @@ -1688,7 +1069,7 @@ exports[`getResource it fetches a resource using locale 1`] = ` } `; -exports[`getResource it fetches raw data 1`] = ` +exports[`getResource() fetches raw data 1`] = ` { "data": { "attributes": { @@ -1898,7 +1279,7 @@ exports[`getResource it fetches raw data 1`] = ` } `; -exports[`getResourceByPath it fetches a resource by path 1`] = ` +exports[`getResourceByPath() fetches a resource by path 1`] = ` { "changed": "2022-03-25T08:02:17+00:00", "content_translation_outdated": false, @@ -2035,7 +1416,7 @@ exports[`getResourceByPath it fetches a resource by path 1`] = ` } `; -exports[`getResourceByPath it fetches a resource by path using locale 1`] = ` +exports[`getResourceByPath() fetches a resource by path using locale 1`] = ` { "field_cooking_time": 30, "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", @@ -2049,7 +1430,7 @@ exports[`getResourceByPath it fetches a resource by path using locale 1`] = ` } `; -exports[`getResourceByPath it fetches a resource by path with params 1`] = ` +exports[`getResourceByPath() fetches a resource by path with params 1`] = ` { "field_cooking_time": 30, "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", @@ -2063,7 +1444,7 @@ exports[`getResourceByPath it fetches a resource by path with params 1`] = ` } `; -exports[`getResourceByPath it fetches raw data 1`] = ` +exports[`getResourceByPath() fetches raw data 1`] = ` { "data": { "attributes": { @@ -2273,7 +1654,7 @@ exports[`getResourceByPath it fetches raw data 1`] = ` } `; -exports[`getResourceCollection it fetches a resource collection 1`] = ` +exports[`getResourceCollection() fetches a resource collection 1`] = ` [ { "id": "52837ad0-f218-46bd-a106-5710336b7053", @@ -2358,7 +1739,7 @@ exports[`getResourceCollection it fetches a resource collection 1`] = ` ] `; -exports[`getResourceCollection it fetches a resource collection using locale 1`] = ` +exports[`getResourceCollection() fetches a resource collection using locale 1`] = ` [ { "id": "52837ad0-f218-46bd-a106-5710336b7053", @@ -2451,7 +1832,7 @@ exports[`getResourceCollection it fetches a resource collection using locale 1`] ] `; -exports[`getResourceCollection it fetches raw data 1`] = ` +exports[`getResourceCollection() fetches raw data 1`] = ` { "data": [ { @@ -2500,207 +1881,152 @@ exports[`getResourceCollection it fetches raw data 1`] = ` } `; -exports[`getResourceCollectionFromContext it fetches a resource collection 1`] = ` +exports[`getSearchIndex() fetches a search index 1`] = ` [ { - "id": "52837ad0-f218-46bd-a106-5710336b7053", + "id": "3873f7b4-790c-4f93-ac7a-b4ca71272128", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/3873f7b4-790c-4f93-ac7a-b4ca71272128?resourceVersion=id%3A18", }, }, - "title": "Give it a go and grow your own herbs", - "type": "node--article", + "title": "Fiery chili sauce", + "type": "node--recipe", }, { - "id": "3d5a7bca-5b4a-49ee-87d2-cbe313562903", + "id": "f4fff2df-1a2a-4762-be23-6b7d5755eba1", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/3d5a7bca-5b4a-49ee-87d2-cbe313562903?resourceVersion=id%3A22", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/f4fff2df-1a2a-4762-be23-6b7d5755eba1?resourceVersion=id%3A10", }, }, - "title": "Dairy-free and delicious milk chocolate", - "type": "node--article", + "title": "Victoria sponge cake", + "type": "node--recipe", }, { - "id": "a1ef61c9-ed15-4b6d-bba2-bf6e5141f961", + "id": "20b576d2-2343-4b94-bd46-445ecc526e48", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/a1ef61c9-ed15-4b6d-bba2-bf6e5141f961?resourceVersion=id%3A24", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/20b576d2-2343-4b94-bd46-445ecc526e48?resourceVersion=id%3A16", }, }, - "title": "The real deal for supermarket savvy shopping", - "type": "node--article", + "title": "Crema catalana", + "type": "node--recipe", }, { - "id": "ac0b8b56-db8d-4322-9b1e-224f22083f0d", + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ac0b8b56-db8d-4322-9b1e-224f22083f0d?resourceVersion=id%3A26", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", }, }, - "title": "The Umami guide to our favorite mushrooms", - "type": "node--article", + "title": "Deep mediterranean quiche - edited", + "type": "node--recipe", }, { - "id": "22874ecc-0443-441b-a3c9-3aa94d85b800", + "id": "8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/22874ecc-0443-441b-a3c9-3aa94d85b800?resourceVersion=id%3A28", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7?resourceVersion=id%3A6", }, }, - "title": "Let's hear it for carrots", - "type": "node--article", + "title": "Super easy vegetarian pasta bake", + "type": "node--recipe", }, { - "id": "ec51b1de-f51e-4761-b061-aa2624e58b4a", + "id": "84e09cbf-e33e-46bd-b40c-4dda4e00937a", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ec51b1de-f51e-4761-b061-aa2624e58b4a?resourceVersion=id%3A30", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/84e09cbf-e33e-46bd-b40c-4dda4e00937a?resourceVersion=id%3A12", }, }, - "title": "Baking mishaps - our troubleshooting tips", - "type": "node--article", + "title": "Gluten free pizza", + "type": "node--recipe", }, { - "id": "d5b9ef80-c0aa-45ca-88e7-79c28abe5d50", + "id": "194f2647-1fbb-42b1-9ea5-3a3eb0494e04", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/d5b9ef80-c0aa-45ca-88e7-79c28abe5d50?resourceVersion=id%3A32", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/194f2647-1fbb-42b1-9ea5-3a3eb0494e04?resourceVersion=id%3A14", }, }, - "title": "Skip the spirits with delicious mocktails", - "type": "node--article", + "title": "Thai green curry", + "type": "node--recipe", }, { - "id": "6c7c249f-cd1a-41d5-944b-b3bfce68b325", + "id": "7862d283-ece2-4ae1-8bef-865259f72315", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/article/6c7c249f-cd1a-41d5-944b-b3bfce68b325?resourceVersion=id%3A34", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/7862d283-ece2-4ae1-8bef-865259f72315?resourceVersion=id%3A4", }, }, - "title": "Give your oatmeal the ultimate makeover", - "type": "node--article", + "title": "Vegan chocolate and nut brownies", + "type": "node--recipe", }, -] -`; - -exports[`getResourceCollectionFromContext it fetches a resource collection using locale 1`] = ` -[ { - "id": "52837ad0-f218-46bd-a106-5710336b7053", - "langcode": "es", + "id": "0bf47bce-51c9-480e-9c24-cd0a457a4f9b", "links": { "self": { - "href": "https://tests.next-drupal.org/es/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", - }, - }, - "title": "Prueba y cultiva tus propias hierbas", - "type": "node--article", - }, - { - "id": "3d5a7bca-5b4a-49ee-87d2-cbe313562903", - "langcode": "es", - "links": { - "self": { - "href": "https://tests.next-drupal.org/es/jsonapi/node/article/3d5a7bca-5b4a-49ee-87d2-cbe313562903?resourceVersion=id%3A22", - }, - }, - "title": "Delicioso chocolate sin lactosa", - "type": "node--article", - }, - { - "id": "a1ef61c9-ed15-4b6d-bba2-bf6e5141f961", - "langcode": "es", - "links": { - "self": { - "href": "https://tests.next-drupal.org/es/jsonapi/node/article/a1ef61c9-ed15-4b6d-bba2-bf6e5141f961?resourceVersion=id%3A24", - }, - }, - "title": "El verdadero negocio para comprar en el supermercado", - "type": "node--article", - }, - { - "id": "ac0b8b56-db8d-4322-9b1e-224f22083f0d", - "langcode": "es", - "links": { - "self": { - "href": "https://tests.next-drupal.org/es/jsonapi/node/article/ac0b8b56-db8d-4322-9b1e-224f22083f0d?resourceVersion=id%3A26", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/0bf47bce-51c9-480e-9c24-cd0a457a4f9b?resourceVersion=id%3A8", }, }, - "title": "Guía Umami de nuestras setas preferidas", - "type": "node--article", + "title": "Watercress soup", + "type": "node--recipe", }, - { - "id": "22874ecc-0443-441b-a3c9-3aa94d85b800", - "langcode": "es", - "links": { - "self": { - "href": "https://tests.next-drupal.org/es/jsonapi/node/article/22874ecc-0443-441b-a3c9-3aa94d85b800?resourceVersion=id%3A28", +] +`; + +exports[`getSearchIndex() fetches a search index with facets filters 1`] = ` +{ + "data": [ + { + "attributes": { + "field_difficulty": "easy", + "title": "Super easy vegetarian pasta bake", }, - }, - "title": "Un aplauso para las zanahorias", - "type": "node--article", - }, - { - "id": "ec51b1de-f51e-4761-b061-aa2624e58b4a", - "langcode": "es", - "links": { - "self": { - "href": "https://tests.next-drupal.org/es/jsonapi/node/article/ec51b1de-f51e-4761-b061-aa2624e58b4a?resourceVersion=id%3A30", + "id": "8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7?resourceVersion=id%3A6", + }, }, + "type": "node--recipe", }, - "title": "Percances al hornear - nuestros consejos para solucionar los problemas", - "type": "node--article", - }, - { - "id": "d5b9ef80-c0aa-45ca-88e7-79c28abe5d50", - "langcode": "es", - "links": { - "self": { - "href": "https://tests.next-drupal.org/es/jsonapi/node/article/d5b9ef80-c0aa-45ca-88e7-79c28abe5d50?resourceVersion=id%3A32", + { + "attributes": { + "field_difficulty": "easy", + "title": "Watercress soup", }, - }, - "title": "Salta los espíritus con deliciosos cócteles sin alcohol", - "type": "node--article", - }, - { - "id": "6c7c249f-cd1a-41d5-944b-b3bfce68b325", - "langcode": "es", - "links": { - "self": { - "href": "https://tests.next-drupal.org/es/jsonapi/node/article/6c7c249f-cd1a-41d5-944b-b3bfce68b325?resourceVersion=id%3A34", + "id": "0bf47bce-51c9-480e-9c24-cd0a457a4f9b", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/0bf47bce-51c9-480e-9c24-cd0a457a4f9b?resourceVersion=id%3A8", + }, }, + "type": "node--recipe", }, - "title": "Dale a tu avena el cambio de imagen definitivo", - "type": "node--article", - }, -] -`; - -exports[`getResourceCollectionFromContext it fetches raw data 1`] = ` -{ - "data": [ { "attributes": { - "title": "Vegan chocolate and nut brownies", + "field_difficulty": "easy", + "title": "Victoria sponge cake", }, - "id": "7862d283-ece2-4ae1-8bef-865259f72315", + "id": "f4fff2df-1a2a-4762-be23-6b7d5755eba1", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/7862d283-ece2-4ae1-8bef-865259f72315?resourceVersion=id%3A4", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/f4fff2df-1a2a-4762-be23-6b7d5755eba1?resourceVersion=id%3A10", }, }, "type": "node--recipe", }, { "attributes": { - "title": "Super easy vegetarian pasta bake", + "field_difficulty": "easy", + "title": "Fiery chili sauce", }, - "id": "8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7", + "id": "3873f7b4-790c-4f93-ac7a-b4ca71272128", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7?resourceVersion=id%3A6", + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/3873f7b4-790c-4f93-ac7a-b4ca71272128?resourceVersion=id%3A18", }, }, "type": "node--recipe", @@ -2716,396 +2042,9 @@ exports[`getResourceCollectionFromContext it fetches raw data 1`] = ` }, "version": "1.0", }, - "links": { - "next": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe?fields%5Bnode--recipe%5D=title&page%5Boffset%5D=2&page%5Blimit%5D=2", - }, - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe?fields%5Bnode--recipe%5D=title&page%5Blimit%5D=2", - }, - }, -} -`; - -exports[`getResourceFromContext it accepts a translated path 1`] = ` -{ - "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", - }, - }, - "path": { - "alias": "/recipes/deep-mediterranean-quiche", - "langcode": "en", - "pid": 67, - }, - "status": true, - "title": "Deep mediterranean quiche - edited", - "type": "node--recipe", -} -`; - -exports[`getResourceFromContext it fetches a resource from context 1`] = ` -{ - "changed": "2022-03-25T08:02:17+00:00", - "content_translation_outdated": false, - "content_translation_source": "und", - "created": "2022-03-21T10:52:42+00:00", - "default_langcode": true, - "drupal_internal__nid": 1, - "drupal_internal__vid": 37, - "field_cooking_time": 30, - "field_difficulty": "medium", - "field_ingredients": [ - "For the pastry:", - "280g plain flour", - "140g butter", - "Cold water", - "For the filling:", - "1 onion", - "2 garlic cloves", - "Half a courgette", - "450ml soya milk", - "500g grated parmesan", - "2 eggs", - "200g sun dried tomatoes", - "100g feta", - ], - "field_media_image": { - "id": "bbfe9d97-2da2-432b-a22c-0396c08e06ca", - "resourceIdObjMeta": { - "drupal_internal__target_id": 1, - }, - "type": "media--image", - }, - "field_number_of_servings": 8, - "field_preparation_time": 40, - "field_recipe_category": [ - { - "id": "a6c02fe4-67bf-462c-90cb-32281a07efe4", - "resourceIdObjMeta": { - "drupal_internal__target_id": 31, - }, - "type": "taxonomy_term--recipe_category", - }, - ], - "field_recipe_instruction": { - "format": "basic_html", - "processed": "
  1. Preheat the oven to 400°F/200°C. Starting with the pastry; rub the flour and butter together in a bowl until crumbling like breadcrumbs. Add water, a little at a time, until it forms a dough.
  2. -
  3. Roll out the pastry on a floured board and gently spread over your tin. Place in the fridge for 20 minutes before blind baking for a further 10.
  4. -
  5. Whilst the pastry is cooling, chop and gently cook the onions, garlic and courgette.
  6. -
  7. In a large bowl, add the soya milk, half the parmesan, and the eggs. Gently mix.
  8. -
  9. Once the pastry is cooked, spread the onions, garlic and sun dried tomatoes over the base and pour the eggs mix over. Sprinkle the remaining parmesan and careful lay the feta over the top. Bake for 30 minutes or until golden brown.
  10. -
", - "value": "
    -
  1. Preheat the oven to 400°F/200°C. Starting with the pastry; rub the flour and butter together in a bowl until crumbling like breadcrumbs. Add water, a little at a time, until it forms a dough.
  2. -
  3. Roll out the pastry on a floured board and gently spread over your tin. Place in the fridge for 20 minutes before blind baking for a further 10.
  4. -
  5. Whilst the pastry is cooling, chop and gently cook the onions, garlic and courgette.
  6. -
  7. In a large bowl, add the soya milk, half the parmesan, and the eggs. Gently mix.
  8. -
  9. Once the pastry is cooked, spread the onions, garlic and sun dried tomatoes over the base and pour the eggs mix over. Sprinkle the remaining parmesan and careful lay the feta over the top. Bake for 30 minutes or until golden brown.
  10. -
-", - }, - "field_summary": { - "format": "basic_html", - "processed": "

An Italian inspired quiche with sun dried tomatoes and courgette. A perfect light meal for a summer's day.

-", - "value": "

An Italian inspired quiche with sun dried tomatoes and courgette. A perfect light meal for a summer's day.

-", - }, - "field_tags": [ - { - "id": "46258827-cfad-4813-99dc-287c4cb41117", - "resourceIdObjMeta": { - "drupal_internal__target_id": 22, - }, - "type": "taxonomy_term--tags", - }, - { - "id": "f32a4d84-0568-4bfd-8be3-8217d36efb6d", - "resourceIdObjMeta": { - "drupal_internal__target_id": 13, - }, - "type": "taxonomy_term--tags", - }, - ], - "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - "langcode": "en", "links": { "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", - }, - }, - "moderation_state": "published", - "node_type": { - "id": "9b70a287-cade-454f-be8b-dea7b9a37c7a", - "resourceIdObjMeta": { - "drupal_internal__target_id": "recipe", - }, - "type": "node_type--node_type", - }, - "path": { - "alias": "/recipes/deep-mediterranean-quiche", - "langcode": "en", - "pid": 67, - }, - "promote": true, - "relationshipNames": [ - "node_type", - "revision_uid", - "uid", - "field_media_image", - "field_recipe_category", - "field_tags", - ], - "revision_log": null, - "revision_timestamp": "2022-03-25T08:02:17+00:00", - "revision_translation_affected": true, - "revision_uid": { - "id": "365cc7b5-ddc4-4b3b-939e-1494400aab4a", - "resourceIdObjMeta": { - "drupal_internal__target_id": 1, - }, - "type": "user--user", - }, - "status": true, - "sticky": false, - "title": "Deep mediterranean quiche - edited", - "type": "node--recipe", - "uid": { - "id": "9e4944e8-dd77-407a-8610-83e823b48b56", - "resourceIdObjMeta": { - "drupal_internal__target_id": 4, - }, - "type": "user--user", - }, -} -`; - -exports[`getResourceFromContext it fetches a resource from context using locale 1`] = ` -{ - "field_cooking_time": 30, - "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - "links": { - "self": { - "href": "https://tests.next-drupal.org/es/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", - }, - }, - "title": "Quiche mediterráneo profundo", - "type": "node--recipe", -} -`; - -exports[`getResourceFromContext it fetches a resource from context with params 1`] = ` -{ - "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", - }, - }, - "title": "Deep mediterranean quiche - edited", - "type": "node--recipe", -} -`; - -exports[`getResourceFromContext it fetches raw data 1`] = ` -{ - "data": { - "attributes": { - "title": "Deep mediterranean quiche - edited", - }, - "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", - }, - }, - "type": "node--recipe", - }, - "jsonapi": { - "meta": { - "links": { - "self": { - "href": "http://jsonapi.org/format/1.0/", - }, - }, - }, - "version": "1.0", - }, - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?fields%5Bnode--recipe%5D=title&resourceVersion=rel%3Alatest-version", - }, - }, -} -`; - -exports[`getSearchIndex it fetches a search index 1`] = ` -[ - { - "id": "3873f7b4-790c-4f93-ac7a-b4ca71272128", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/3873f7b4-790c-4f93-ac7a-b4ca71272128?resourceVersion=id%3A18", - }, - }, - "title": "Fiery chili sauce", - "type": "node--recipe", - }, - { - "id": "f4fff2df-1a2a-4762-be23-6b7d5755eba1", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/f4fff2df-1a2a-4762-be23-6b7d5755eba1?resourceVersion=id%3A10", - }, - }, - "title": "Victoria sponge cake", - "type": "node--recipe", - }, - { - "id": "20b576d2-2343-4b94-bd46-445ecc526e48", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/20b576d2-2343-4b94-bd46-445ecc526e48?resourceVersion=id%3A16", - }, - }, - "title": "Crema catalana", - "type": "node--recipe", - }, - { - "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", - }, - }, - "title": "Deep mediterranean quiche - edited", - "type": "node--recipe", - }, - { - "id": "8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7?resourceVersion=id%3A6", - }, - }, - "title": "Super easy vegetarian pasta bake", - "type": "node--recipe", - }, - { - "id": "84e09cbf-e33e-46bd-b40c-4dda4e00937a", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/84e09cbf-e33e-46bd-b40c-4dda4e00937a?resourceVersion=id%3A12", - }, - }, - "title": "Gluten free pizza", - "type": "node--recipe", - }, - { - "id": "194f2647-1fbb-42b1-9ea5-3a3eb0494e04", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/194f2647-1fbb-42b1-9ea5-3a3eb0494e04?resourceVersion=id%3A14", - }, - }, - "title": "Thai green curry", - "type": "node--recipe", - }, - { - "id": "7862d283-ece2-4ae1-8bef-865259f72315", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/7862d283-ece2-4ae1-8bef-865259f72315?resourceVersion=id%3A4", - }, - }, - "title": "Vegan chocolate and nut brownies", - "type": "node--recipe", - }, - { - "id": "0bf47bce-51c9-480e-9c24-cd0a457a4f9b", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/0bf47bce-51c9-480e-9c24-cd0a457a4f9b?resourceVersion=id%3A8", - }, - }, - "title": "Watercress soup", - "type": "node--recipe", - }, -] -`; - -exports[`getSearchIndex it fetches a search index with facets filters 1`] = ` -{ - "data": [ - { - "attributes": { - "field_difficulty": "easy", - "title": "Super easy vegetarian pasta bake", - }, - "id": "8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7?resourceVersion=id%3A6", - }, - }, - "type": "node--recipe", - }, - { - "attributes": { - "field_difficulty": "easy", - "title": "Watercress soup", - }, - "id": "0bf47bce-51c9-480e-9c24-cd0a457a4f9b", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/0bf47bce-51c9-480e-9c24-cd0a457a4f9b?resourceVersion=id%3A8", - }, - }, - "type": "node--recipe", - }, - { - "attributes": { - "field_difficulty": "easy", - "title": "Victoria sponge cake", - }, - "id": "f4fff2df-1a2a-4762-be23-6b7d5755eba1", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/f4fff2df-1a2a-4762-be23-6b7d5755eba1?resourceVersion=id%3A10", - }, - }, - "type": "node--recipe", - }, - { - "attributes": { - "field_difficulty": "easy", - "title": "Fiery chili sauce", - }, - "id": "3873f7b4-790c-4f93-ac7a-b4ca71272128", - "links": { - "self": { - "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/3873f7b4-790c-4f93-ac7a-b4ca71272128?resourceVersion=id%3A18", - }, - }, - "type": "node--recipe", - }, - ], - "jsonapi": { - "meta": { - "links": { - "self": { - "href": "http://jsonapi.org/format/1.0/", - }, - }, - }, - "version": "1.0", - }, - "links": { - "self": { - "href": "https://tests.next-drupal.org/jsonapi/index/recipes?fields%5Bnode--recipe%5D=title%2Cfield_difficulty&filter%5Bdifficulty%5D=easy", + "href": "https://tests.next-drupal.org/jsonapi/index/recipes?fields%5Bnode--recipe%5D=title%2Cfield_difficulty&filter%5Bdifficulty%5D=easy", }, }, "meta": { @@ -3966,7 +2905,7 @@ exports[`getSearchIndex it fetches a search index with facets filters 1`] = ` } `; -exports[`getSearchIndex it fetches a search index with locale 1`] = ` +exports[`getSearchIndex() fetches a search index with locale 1`] = ` [ { "id": "3873f7b4-790c-4f93-ac7a-b4ca71272128", @@ -4061,7 +3000,7 @@ exports[`getSearchIndex it fetches a search index with locale 1`] = ` ] `; -exports[`getSearchIndex it fetches raw data from search index 1`] = ` +exports[`getSearchIndex() fetches raw data from search index 1`] = ` { "data": [ { @@ -4788,807 +3727,209 @@ exports[`getSearchIndex it fetches raw data from search index 1`] = ` "data-drupal-facet-item-id": "title-watercress", "data-drupal-facet-item-value": "watercress", }, - "#title": { - "#count": 1, - "#facet": { - "show_title": null, - }, - "#is_active": false, - "#raw_value": "watercress", - "#show_count": false, - "#theme": "facets_result_item", - "#value": "watercress", - }, - "#type": "link", - "#url": {}, - "#wrapper_attributes": { - "class": [ - "facet-item", - ], - }, - }, - "3": { - "#attributes": { - "data-drupal-facet-item-count": 1, - "data-drupal-facet-item-id": "title-al", - "data-drupal-facet-item-value": "al", - }, - "#title": { - "#count": 1, - "#facet": { - "show_title": null, - }, - "#is_active": false, - "#raw_value": "al", - "#show_count": false, - "#theme": "facets_result_item", - "#value": "al", - }, - "#type": "link", - "#url": {}, - "#wrapper_attributes": { - "class": [ - "facet-item", - ], - }, - }, - "4": { - "#attributes": { - "data-drupal-facet-item-count": 1, - "data-drupal-facet-item-id": "title-ardiente", - "data-drupal-facet-item-value": "ardiente", - }, - "#title": { - "#count": 1, - "#facet": { - "show_title": null, - }, - "#is_active": false, - "#raw_value": "ardiente", - "#show_count": false, - "#theme": "facets_result_item", - "#value": "ardiente", - }, - "#type": "link", - "#url": {}, - "#wrapper_attributes": { - "class": [ - "facet-item", - ], - }, - }, - "5": { - "#attributes": { - "data-drupal-facet-item-count": 1, - "data-drupal-facet-item-id": "title-bake", - "data-drupal-facet-item-value": "bake", - }, - "#title": { - "#count": 1, - "#facet": { - "show_title": null, - }, - "#is_active": false, - "#raw_value": "bake", - "#show_count": false, - "#theme": "facets_result_item", - "#value": "bake", - }, - "#type": "link", - "#url": {}, - "#wrapper_attributes": { - "class": [ - "facet-item", - ], - }, - }, - "6": { - "#attributes": { - "data-drupal-facet-item-count": 1, - "data-drupal-facet-item-id": "title-berro", - "data-drupal-facet-item-value": "berro", - }, - "#title": { - "#count": 1, - "#facet": { - "show_title": null, - }, - "#is_active": false, - "#raw_value": "berro", - "#show_count": false, - "#theme": "facets_result_item", - "#value": "berro", - }, - "#type": "link", - "#url": {}, - "#wrapper_attributes": { - "class": [ - "facet-item", - ], - }, - }, - "7": { - "#attributes": { - "data-drupal-facet-item-count": 1, - "data-drupal-facet-item-id": "title-cake", - "data-drupal-facet-item-value": "cake", - }, - "#title": { - "#count": 1, - "#facet": { - "show_title": null, - }, - "#is_active": false, - "#raw_value": "cake", - "#show_count": false, - "#theme": "facets_result_item", - "#value": "cake", - }, - "#type": "link", - "#url": {}, - "#wrapper_attributes": { - "class": [ - "facet-item", - ], - }, - }, - "8": { - "#attributes": { - "data-drupal-facet-item-count": 1, - "data-drupal-facet-item-id": "title-chile", - "data-drupal-facet-item-value": "chile", - }, - "#title": { - "#count": 1, - "#facet": { - "show_title": null, - }, - "#is_active": false, - "#raw_value": "chile", - "#show_count": false, - "#theme": "facets_result_item", - "#value": "chile", - }, - "#type": "link", - "#url": {}, - "#wrapper_attributes": { - "class": [ - "facet-item", - ], - }, - }, - "9": { - "#attributes": { - "data-drupal-facet-item-count": 1, - "data-drupal-facet-item-id": "title-chili", - "data-drupal-facet-item-value": "chili", - }, - "#title": { - "#count": 1, - "#facet": { - "show_title": null, - }, - "#is_active": false, - "#raw_value": "chili", - "#show_count": false, - "#theme": "facets_result_item", - "#value": "chili", - }, - "#type": "link", - "#url": {}, - "#wrapper_attributes": { - "class": [ - "facet-item", - ], - }, - }, - }, - "#theme": "facets_item_list__links__title", - }, - ], - }, -} -`; - -exports[`getStaticPathsFromContext it returns static paths for multiple resource types from context 1`] = ` -[ - { - "locale": "en", - "params": { - "slug": [ - "articles", - "give-it-a-go-and-grow-your-own-herbs", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "dairy-free-and-delicious-milk-chocolate", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "the-real-deal-for-supermarket-savvy-shopping", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "the-umami-guide-to-our-favourite-mushrooms", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "lets-hear-it-for-carrots", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "baking-mishaps-our-troubleshooting-tips", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "skip-the-spirits-with-delicious-mocktails", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "give-your-oatmeal-the-ultimate-makeover", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "prueba-y-cultiva-tus-propias-hierbas", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "delicioso-chocolate-sin-lactosa", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "el-verdadeo-negocio-para-comprar-en-el-supermercado", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "guia-umami-de-nuestras-setas-preferidas", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "un-aplauso-para-las-zanahorias", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "percances-al-hornear-nuestros-consejos-para-solucionar-problemas", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "salta-los-espiritus-con-deliciosos-cocteles-sin-alcohol", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "dale-a-tu-avena-el-cambio-de-imagen-definitivo", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "recipes", - "vegan-chocolate-and-nut-brownies", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "recipes", - "super-easy-vegetarian-pasta-bake", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "recipes", - "watercress-soup", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "recipes", - "victoria-sponge-cake", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "recipes", - "gluten-free-pizza", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "recipes", - "thai-green-curry", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "recipes", - "crema-catalana", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "recipes", - "fiery-chili-sauce", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "recipes", - "deep-mediterranean-quiche", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "recipes", - "bizcochos-veganos-de-chocolate-y-nueces", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "recipes", - "pasta-vegetariana-horno-super-facil", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "recipes", - "sopa-de-berro", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "recipes", - "pastel-victoria", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "recipes", - "pizza-sin-gluten", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "recipes", - "curry-verde-tailandes", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "recipes", - "crema-catalana", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "recipes", - "salsa-de-chile-ardiente", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "recipes", - "quiche-mediterráneo-profundo", - ], - }, - }, -] -`; - -exports[`getStaticPathsFromContext it returns static paths from context 1`] = ` -[ - { - "params": { - "slug": [ - "articles", - "give-it-a-go-and-grow-your-own-herbs", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "dairy-free-and-delicious-milk-chocolate", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "the-real-deal-for-supermarket-savvy-shopping", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "the-umami-guide-to-our-favourite-mushrooms", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "lets-hear-it-for-carrots", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "baking-mishaps-our-troubleshooting-tips", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "skip-the-spirits-with-delicious-mocktails", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "give-your-oatmeal-the-ultimate-makeover", - ], - }, - }, -] -`; - -exports[`getStaticPathsFromContext it returns static paths from context with locale 1`] = ` -[ - { - "locale": "en", - "params": { - "slug": [ - "articles", - "give-it-a-go-and-grow-your-own-herbs", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "dairy-free-and-delicious-milk-chocolate", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "the-real-deal-for-supermarket-savvy-shopping", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "the-umami-guide-to-our-favourite-mushrooms", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "lets-hear-it-for-carrots", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "baking-mishaps-our-troubleshooting-tips", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "skip-the-spirits-with-delicious-mocktails", - ], - }, - }, - { - "locale": "en", - "params": { - "slug": [ - "articles", - "give-your-oatmeal-the-ultimate-makeover", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "prueba-y-cultiva-tus-propias-hierbas", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "delicioso-chocolate-sin-lactosa", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "el-verdadeo-negocio-para-comprar-en-el-supermercado", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "guia-umami-de-nuestras-setas-preferidas", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "un-aplauso-para-las-zanahorias", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "percances-al-hornear-nuestros-consejos-para-solucionar-problemas", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "salta-los-espiritus-con-deliciosos-cocteles-sin-alcohol", - ], - }, - }, - { - "locale": "es", - "params": { - "slug": [ - "articles", - "dale-a-tu-avena-el-cambio-de-imagen-definitivo", - ], - }, - }, -] -`; - -exports[`getStaticPathsFromContext it returns static paths from context with params 1`] = ` -[ - { - "params": { - "slug": [ - "articles", - "give-it-a-go-and-grow-your-own-herbs", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "dairy-free-and-delicious-milk-chocolate", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "the-real-deal-for-supermarket-savvy-shopping", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "the-umami-guide-to-our-favourite-mushrooms", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "lets-hear-it-for-carrots", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "baking-mishaps-our-troubleshooting-tips", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "skip-the-spirits-with-delicious-mocktails", - ], - }, - }, - { - "params": { - "slug": [ - "articles", - "give-your-oatmeal-the-ultimate-makeover", - ], - }, + "#title": { + "#count": 1, + "#facet": { + "show_title": null, + }, + "#is_active": false, + "#raw_value": "watercress", + "#show_count": false, + "#theme": "facets_result_item", + "#value": "watercress", + }, + "#type": "link", + "#url": {}, + "#wrapper_attributes": { + "class": [ + "facet-item", + ], + }, + }, + "3": { + "#attributes": { + "data-drupal-facet-item-count": 1, + "data-drupal-facet-item-id": "title-al", + "data-drupal-facet-item-value": "al", + }, + "#title": { + "#count": 1, + "#facet": { + "show_title": null, + }, + "#is_active": false, + "#raw_value": "al", + "#show_count": false, + "#theme": "facets_result_item", + "#value": "al", + }, + "#type": "link", + "#url": {}, + "#wrapper_attributes": { + "class": [ + "facet-item", + ], + }, + }, + "4": { + "#attributes": { + "data-drupal-facet-item-count": 1, + "data-drupal-facet-item-id": "title-ardiente", + "data-drupal-facet-item-value": "ardiente", + }, + "#title": { + "#count": 1, + "#facet": { + "show_title": null, + }, + "#is_active": false, + "#raw_value": "ardiente", + "#show_count": false, + "#theme": "facets_result_item", + "#value": "ardiente", + }, + "#type": "link", + "#url": {}, + "#wrapper_attributes": { + "class": [ + "facet-item", + ], + }, + }, + "5": { + "#attributes": { + "data-drupal-facet-item-count": 1, + "data-drupal-facet-item-id": "title-bake", + "data-drupal-facet-item-value": "bake", + }, + "#title": { + "#count": 1, + "#facet": { + "show_title": null, + }, + "#is_active": false, + "#raw_value": "bake", + "#show_count": false, + "#theme": "facets_result_item", + "#value": "bake", + }, + "#type": "link", + "#url": {}, + "#wrapper_attributes": { + "class": [ + "facet-item", + ], + }, + }, + "6": { + "#attributes": { + "data-drupal-facet-item-count": 1, + "data-drupal-facet-item-id": "title-berro", + "data-drupal-facet-item-value": "berro", + }, + "#title": { + "#count": 1, + "#facet": { + "show_title": null, + }, + "#is_active": false, + "#raw_value": "berro", + "#show_count": false, + "#theme": "facets_result_item", + "#value": "berro", + }, + "#type": "link", + "#url": {}, + "#wrapper_attributes": { + "class": [ + "facet-item", + ], + }, + }, + "7": { + "#attributes": { + "data-drupal-facet-item-count": 1, + "data-drupal-facet-item-id": "title-cake", + "data-drupal-facet-item-value": "cake", + }, + "#title": { + "#count": 1, + "#facet": { + "show_title": null, + }, + "#is_active": false, + "#raw_value": "cake", + "#show_count": false, + "#theme": "facets_result_item", + "#value": "cake", + }, + "#type": "link", + "#url": {}, + "#wrapper_attributes": { + "class": [ + "facet-item", + ], + }, + }, + "8": { + "#attributes": { + "data-drupal-facet-item-count": 1, + "data-drupal-facet-item-id": "title-chile", + "data-drupal-facet-item-value": "chile", + }, + "#title": { + "#count": 1, + "#facet": { + "show_title": null, + }, + "#is_active": false, + "#raw_value": "chile", + "#show_count": false, + "#theme": "facets_result_item", + "#value": "chile", + }, + "#type": "link", + "#url": {}, + "#wrapper_attributes": { + "class": [ + "facet-item", + ], + }, + }, + "9": { + "#attributes": { + "data-drupal-facet-item-count": 1, + "data-drupal-facet-item-id": "title-chili", + "data-drupal-facet-item-value": "chili", + }, + "#title": { + "#count": 1, + "#facet": { + "show_title": null, + }, + "#is_active": false, + "#raw_value": "chili", + "#show_count": false, + "#theme": "facets_result_item", + "#value": "chili", + }, + "#type": "link", + "#url": {}, + "#wrapper_attributes": { + "class": [ + "facet-item", + ], + }, + }, + }, + "#theme": "facets_item_list__links__title", + }, + ], }, -] +} `; -exports[`getView it fetches a view 1`] = ` +exports[`getView() fetches a view 1`] = ` { "id": "featured_articles--page_1", "links": { @@ -6617,7 +4958,7 @@ This low-sugar delight will meet all of those chocolate cravings and is the perf } `; -exports[`getView it fetches a view with locale 1`] = ` +exports[`getView() fetches a view with locale 1`] = ` { "id": "featured_articles--page_1", "links": { @@ -6713,7 +5054,7 @@ exports[`getView it fetches a view with locale 1`] = ` } `; -exports[`getView it fetches a view with params 1`] = ` +exports[`getView() fetches a view with params 1`] = ` { "id": "featured_articles--page_1", "links": { @@ -6809,7 +5150,7 @@ exports[`getView it fetches a view with params 1`] = ` } `; -exports[`getView it fetches raw data 1`] = ` +exports[`getView() fetches raw data 1`] = ` { "id": "featured_articles--page_1", "links": { @@ -6941,65 +5282,7 @@ exports[`getView it fetches raw data 1`] = ` } `; -exports[`translatePath it translates a path 1`] = ` -{ - "entity": { - "bundle": "recipe", - "canonical": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", - "id": "1", - "langcode": "en", - "path": "/en/recipes/deep-mediterranean-quiche", - "type": "node", - "uuid": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - }, - "isHomePath": false, - "jsonapi": { - "basePath": "/en/jsonapi", - "entryPoint": "https://tests.next-drupal.org/en/jsonapi", - "individual": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f", - "pathPrefix": "en/jsonapi", - "resourceName": "node--recipe", - }, - "label": "Deep mediterranean quiche - edited", - "meta": { - "deprecated": { - "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.", - }, - }, - "resolved": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", -} -`; - -exports[`translatePathFromContext it translates a path 1`] = ` -{ - "entity": { - "bundle": "recipe", - "canonical": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", - "id": "1", - "langcode": "en", - "path": "/en/recipes/deep-mediterranean-quiche", - "type": "node", - "uuid": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - }, - "isHomePath": false, - "jsonapi": { - "basePath": "/en/jsonapi", - "entryPoint": "https://tests.next-drupal.org/en/jsonapi", - "individual": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f", - "pathPrefix": "en/jsonapi", - "resourceName": "node--recipe", - }, - "label": "Deep mediterranean quiche - edited", - "meta": { - "deprecated": { - "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.", - }, - }, - "resolved": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", -} -`; - -exports[`translatePathFromContext it translates a path with pathPrefix 1`] = ` +exports[`translatePath() translates a path 1`] = ` { "entity": { "bundle": "recipe", diff --git a/packages/next-drupal/tests/DrupalClient/__snapshots__/pages-router-methods.test.ts.snap b/packages/next-drupal/tests/DrupalClient/__snapshots__/pages-router-methods.test.ts.snap new file mode 100644 index 00000000..03312b32 --- /dev/null +++ b/packages/next-drupal/tests/DrupalClient/__snapshots__/pages-router-methods.test.ts.snap @@ -0,0 +1,2255 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getPathFromContext() encodes path with punctuation 1`] = ` +{ + "entity": { + "bundle": "page", + "canonical": "https://tests.next-drupal.org/en/path%26with%5Epunc%26in%24path", + "id": "20", + "langcode": "en", + "path": "/en/path%26with%5Epunc%26in%24path", + "type": "node", + "uuid": "e4bbe727-14ee-44bc-a34a-14ce5c86a82e", + }, + "isHomePath": false, + "jsonapi": { + "basePath": "/en/jsonapi", + "entryPoint": "https://tests.next-drupal.org/en/jsonapi", + "individual": "https://tests.next-drupal.org/en/jsonapi/node/page/e4bbe727-14ee-44bc-a34a-14ce5c86a82e", + "pathPrefix": "en/jsonapi", + "resourceName": "node--page", + }, + "label": "Page with characters in path", + "meta": { + "deprecated": { + "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.", + }, + }, + "resolved": "https://tests.next-drupal.org/en/path%26with%5Epunc%26in%24path", +} +`; + +exports[`getResourceCollectionFromContext() fetches a resource collection 1`] = ` +[ + { + "id": "52837ad0-f218-46bd-a106-5710336b7053", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", + }, + }, + "title": "Give it a go and grow your own herbs", + "type": "node--article", + }, + { + "id": "3d5a7bca-5b4a-49ee-87d2-cbe313562903", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/3d5a7bca-5b4a-49ee-87d2-cbe313562903?resourceVersion=id%3A22", + }, + }, + "title": "Dairy-free and delicious milk chocolate", + "type": "node--article", + }, + { + "id": "a1ef61c9-ed15-4b6d-bba2-bf6e5141f961", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/a1ef61c9-ed15-4b6d-bba2-bf6e5141f961?resourceVersion=id%3A24", + }, + }, + "title": "The real deal for supermarket savvy shopping", + "type": "node--article", + }, + { + "id": "ac0b8b56-db8d-4322-9b1e-224f22083f0d", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ac0b8b56-db8d-4322-9b1e-224f22083f0d?resourceVersion=id%3A26", + }, + }, + "title": "The Umami guide to our favorite mushrooms", + "type": "node--article", + }, + { + "id": "22874ecc-0443-441b-a3c9-3aa94d85b800", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/22874ecc-0443-441b-a3c9-3aa94d85b800?resourceVersion=id%3A28", + }, + }, + "title": "Let's hear it for carrots", + "type": "node--article", + }, + { + "id": "ec51b1de-f51e-4761-b061-aa2624e58b4a", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ec51b1de-f51e-4761-b061-aa2624e58b4a?resourceVersion=id%3A30", + }, + }, + "title": "Baking mishaps - our troubleshooting tips", + "type": "node--article", + }, + { + "id": "d5b9ef80-c0aa-45ca-88e7-79c28abe5d50", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/d5b9ef80-c0aa-45ca-88e7-79c28abe5d50?resourceVersion=id%3A32", + }, + }, + "title": "Skip the spirits with delicious mocktails", + "type": "node--article", + }, + { + "id": "6c7c249f-cd1a-41d5-944b-b3bfce68b325", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/6c7c249f-cd1a-41d5-944b-b3bfce68b325?resourceVersion=id%3A34", + }, + }, + "title": "Give your oatmeal the ultimate makeover", + "type": "node--article", + }, +] +`; + +exports[`getResourceCollectionFromContext() fetches a resource collection using locale 1`] = ` +[ + { + "id": "52837ad0-f218-46bd-a106-5710336b7053", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", + }, + }, + "title": "Prueba y cultiva tus propias hierbas", + "type": "node--article", + }, + { + "id": "3d5a7bca-5b4a-49ee-87d2-cbe313562903", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/3d5a7bca-5b4a-49ee-87d2-cbe313562903?resourceVersion=id%3A22", + }, + }, + "title": "Delicioso chocolate sin lactosa", + "type": "node--article", + }, + { + "id": "a1ef61c9-ed15-4b6d-bba2-bf6e5141f961", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/a1ef61c9-ed15-4b6d-bba2-bf6e5141f961?resourceVersion=id%3A24", + }, + }, + "title": "El verdadero negocio para comprar en el supermercado", + "type": "node--article", + }, + { + "id": "ac0b8b56-db8d-4322-9b1e-224f22083f0d", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/ac0b8b56-db8d-4322-9b1e-224f22083f0d?resourceVersion=id%3A26", + }, + }, + "title": "Guía Umami de nuestras setas preferidas", + "type": "node--article", + }, + { + "id": "22874ecc-0443-441b-a3c9-3aa94d85b800", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/22874ecc-0443-441b-a3c9-3aa94d85b800?resourceVersion=id%3A28", + }, + }, + "title": "Un aplauso para las zanahorias", + "type": "node--article", + }, + { + "id": "ec51b1de-f51e-4761-b061-aa2624e58b4a", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/ec51b1de-f51e-4761-b061-aa2624e58b4a?resourceVersion=id%3A30", + }, + }, + "title": "Percances al hornear - nuestros consejos para solucionar los problemas", + "type": "node--article", + }, + { + "id": "d5b9ef80-c0aa-45ca-88e7-79c28abe5d50", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/d5b9ef80-c0aa-45ca-88e7-79c28abe5d50?resourceVersion=id%3A32", + }, + }, + "title": "Salta los espíritus con deliciosos cócteles sin alcohol", + "type": "node--article", + }, + { + "id": "6c7c249f-cd1a-41d5-944b-b3bfce68b325", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/6c7c249f-cd1a-41d5-944b-b3bfce68b325?resourceVersion=id%3A34", + }, + }, + "title": "Dale a tu avena el cambio de imagen definitivo", + "type": "node--article", + }, +] +`; + +exports[`getResourceCollectionFromContext() fetches raw data 1`] = ` +{ + "data": [ + { + "attributes": { + "title": "Vegan chocolate and nut brownies", + }, + "id": "7862d283-ece2-4ae1-8bef-865259f72315", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/7862d283-ece2-4ae1-8bef-865259f72315?resourceVersion=id%3A4", + }, + }, + "type": "node--recipe", + }, + { + "attributes": { + "title": "Super easy vegetarian pasta bake", + }, + "id": "8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7?resourceVersion=id%3A6", + }, + }, + "type": "node--recipe", + }, + ], + "jsonapi": { + "meta": { + "links": { + "self": { + "href": "http://jsonapi.org/format/1.0/", + }, + }, + }, + "version": "1.0", + }, + "links": { + "next": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe?fields%5Bnode--recipe%5D=title&page%5Boffset%5D=2&page%5Blimit%5D=2", + }, + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe?fields%5Bnode--recipe%5D=title&page%5Blimit%5D=2", + }, + }, +} +`; + +exports[`getResourceFromContext() accepts a translated path 1`] = ` +{ + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "path": { + "alias": "/recipes/deep-mediterranean-quiche", + "langcode": "en", + "pid": 67, + }, + "status": true, + "title": "Deep mediterranean quiche - edited", + "type": "node--recipe", +} +`; + +exports[`getResourceFromContext() fetches a resource from context 1`] = ` +{ + "changed": "2022-03-25T08:02:17+00:00", + "content_translation_outdated": false, + "content_translation_source": "und", + "created": "2022-03-21T10:52:42+00:00", + "default_langcode": true, + "drupal_internal__nid": 1, + "drupal_internal__vid": 37, + "field_cooking_time": 30, + "field_difficulty": "medium", + "field_ingredients": [ + "For the pastry:", + "280g plain flour", + "140g butter", + "Cold water", + "For the filling:", + "1 onion", + "2 garlic cloves", + "Half a courgette", + "450ml soya milk", + "500g grated parmesan", + "2 eggs", + "200g sun dried tomatoes", + "100g feta", + ], + "field_media_image": { + "id": "bbfe9d97-2da2-432b-a22c-0396c08e06ca", + "resourceIdObjMeta": { + "drupal_internal__target_id": 1, + }, + "type": "media--image", + }, + "field_number_of_servings": 8, + "field_preparation_time": 40, + "field_recipe_category": [ + { + "id": "a6c02fe4-67bf-462c-90cb-32281a07efe4", + "resourceIdObjMeta": { + "drupal_internal__target_id": 31, + }, + "type": "taxonomy_term--recipe_category", + }, + ], + "field_recipe_instruction": { + "format": "basic_html", + "processed": "
  1. Preheat the oven to 400°F/200°C. Starting with the pastry; rub the flour and butter together in a bowl until crumbling like breadcrumbs. Add water, a little at a time, until it forms a dough.
  2. +
  3. Roll out the pastry on a floured board and gently spread over your tin. Place in the fridge for 20 minutes before blind baking for a further 10.
  4. +
  5. Whilst the pastry is cooling, chop and gently cook the onions, garlic and courgette.
  6. +
  7. In a large bowl, add the soya milk, half the parmesan, and the eggs. Gently mix.
  8. +
  9. Once the pastry is cooked, spread the onions, garlic and sun dried tomatoes over the base and pour the eggs mix over. Sprinkle the remaining parmesan and careful lay the feta over the top. Bake for 30 minutes or until golden brown.
  10. +
", + "value": "
    +
  1. Preheat the oven to 400°F/200°C. Starting with the pastry; rub the flour and butter together in a bowl until crumbling like breadcrumbs. Add water, a little at a time, until it forms a dough.
  2. +
  3. Roll out the pastry on a floured board and gently spread over your tin. Place in the fridge for 20 minutes before blind baking for a further 10.
  4. +
  5. Whilst the pastry is cooling, chop and gently cook the onions, garlic and courgette.
  6. +
  7. In a large bowl, add the soya milk, half the parmesan, and the eggs. Gently mix.
  8. +
  9. Once the pastry is cooked, spread the onions, garlic and sun dried tomatoes over the base and pour the eggs mix over. Sprinkle the remaining parmesan and careful lay the feta over the top. Bake for 30 minutes or until golden brown.
  10. +
+", + }, + "field_summary": { + "format": "basic_html", + "processed": "

An Italian inspired quiche with sun dried tomatoes and courgette. A perfect light meal for a summer's day.

+", + "value": "

An Italian inspired quiche with sun dried tomatoes and courgette. A perfect light meal for a summer's day.

+", + }, + "field_tags": [ + { + "id": "46258827-cfad-4813-99dc-287c4cb41117", + "resourceIdObjMeta": { + "drupal_internal__target_id": 22, + }, + "type": "taxonomy_term--tags", + }, + { + "id": "f32a4d84-0568-4bfd-8be3-8217d36efb6d", + "resourceIdObjMeta": { + "drupal_internal__target_id": 13, + }, + "type": "taxonomy_term--tags", + }, + ], + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "langcode": "en", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "moderation_state": "published", + "node_type": { + "id": "9b70a287-cade-454f-be8b-dea7b9a37c7a", + "resourceIdObjMeta": { + "drupal_internal__target_id": "recipe", + }, + "type": "node_type--node_type", + }, + "path": { + "alias": "/recipes/deep-mediterranean-quiche", + "langcode": "en", + "pid": 67, + }, + "promote": true, + "relationshipNames": [ + "node_type", + "revision_uid", + "uid", + "field_media_image", + "field_recipe_category", + "field_tags", + ], + "revision_log": null, + "revision_timestamp": "2022-03-25T08:02:17+00:00", + "revision_translation_affected": true, + "revision_uid": { + "id": "365cc7b5-ddc4-4b3b-939e-1494400aab4a", + "resourceIdObjMeta": { + "drupal_internal__target_id": 1, + }, + "type": "user--user", + }, + "status": true, + "sticky": false, + "title": "Deep mediterranean quiche - edited", + "type": "node--recipe", + "uid": { + "id": "9e4944e8-dd77-407a-8610-83e823b48b56", + "resourceIdObjMeta": { + "drupal_internal__target_id": 4, + }, + "type": "user--user", + }, +} +`; + +exports[`getResourceFromContext() fetches a resource from context using locale 1`] = ` +{ + "field_cooking_time": 30, + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "title": "Quiche mediterráneo profundo", + "type": "node--recipe", +} +`; + +exports[`getResourceFromContext() fetches a resource from context with params 1`] = ` +{ + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "title": "Deep mediterranean quiche - edited", + "type": "node--recipe", +} +`; + +exports[`getResourceFromContext() fetches raw data 1`] = ` +{ + "data": { + "attributes": { + "title": "Deep mediterranean quiche - edited", + }, + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "type": "node--recipe", + }, + "jsonapi": { + "meta": { + "links": { + "self": { + "href": "http://jsonapi.org/format/1.0/", + }, + }, + }, + "version": "1.0", + }, + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?fields%5Bnode--recipe%5D=title&resourceVersion=rel%3Alatest-version", + }, + }, +} +`; + +exports[`getStaticPathsFromContext() returns static paths for multiple resource types from context 1`] = ` +[ + { + "locale": "en", + "params": { + "slug": [ + "articles", + "give-it-a-go-and-grow-your-own-herbs", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "dairy-free-and-delicious-milk-chocolate", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "the-real-deal-for-supermarket-savvy-shopping", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "the-umami-guide-to-our-favourite-mushrooms", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "lets-hear-it-for-carrots", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "baking-mishaps-our-troubleshooting-tips", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "skip-the-spirits-with-delicious-mocktails", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "give-your-oatmeal-the-ultimate-makeover", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "prueba-y-cultiva-tus-propias-hierbas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "delicioso-chocolate-sin-lactosa", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "el-verdadeo-negocio-para-comprar-en-el-supermercado", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "guia-umami-de-nuestras-setas-preferidas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "un-aplauso-para-las-zanahorias", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "percances-al-hornear-nuestros-consejos-para-solucionar-problemas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "salta-los-espiritus-con-deliciosos-cocteles-sin-alcohol", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "dale-a-tu-avena-el-cambio-de-imagen-definitivo", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "vegan-chocolate-and-nut-brownies", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "super-easy-vegetarian-pasta-bake", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "watercress-soup", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "victoria-sponge-cake", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "gluten-free-pizza", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "thai-green-curry", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "crema-catalana", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "fiery-chili-sauce", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "deep-mediterranean-quiche", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "bizcochos-veganos-de-chocolate-y-nueces", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "pasta-vegetariana-horno-super-facil", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "sopa-de-berro", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "pastel-victoria", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "pizza-sin-gluten", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "curry-verde-tailandes", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "crema-catalana", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "salsa-de-chile-ardiente", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "quiche-mediterráneo-profundo", + ], + }, + }, +] +`; + +exports[`getStaticPathsFromContext() returns static paths from context 1`] = ` +[ + { + "params": { + "slug": [ + "articles", + "give-it-a-go-and-grow-your-own-herbs", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "dairy-free-and-delicious-milk-chocolate", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "the-real-deal-for-supermarket-savvy-shopping", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "the-umami-guide-to-our-favourite-mushrooms", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "lets-hear-it-for-carrots", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "baking-mishaps-our-troubleshooting-tips", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "skip-the-spirits-with-delicious-mocktails", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "give-your-oatmeal-the-ultimate-makeover", + ], + }, + }, +] +`; + +exports[`getStaticPathsFromContext() returns static paths from context with locale 1`] = ` +[ + { + "locale": "en", + "params": { + "slug": [ + "articles", + "give-it-a-go-and-grow-your-own-herbs", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "dairy-free-and-delicious-milk-chocolate", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "the-real-deal-for-supermarket-savvy-shopping", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "the-umami-guide-to-our-favourite-mushrooms", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "lets-hear-it-for-carrots", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "baking-mishaps-our-troubleshooting-tips", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "skip-the-spirits-with-delicious-mocktails", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "give-your-oatmeal-the-ultimate-makeover", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "prueba-y-cultiva-tus-propias-hierbas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "delicioso-chocolate-sin-lactosa", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "el-verdadeo-negocio-para-comprar-en-el-supermercado", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "guia-umami-de-nuestras-setas-preferidas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "un-aplauso-para-las-zanahorias", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "percances-al-hornear-nuestros-consejos-para-solucionar-problemas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "salta-los-espiritus-con-deliciosos-cocteles-sin-alcohol", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "dale-a-tu-avena-el-cambio-de-imagen-definitivo", + ], + }, + }, +] +`; + +exports[`getStaticPathsFromContext() returns static paths from context with params 1`] = ` +[ + { + "params": { + "slug": [ + "articles", + "give-it-a-go-and-grow-your-own-herbs", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "dairy-free-and-delicious-milk-chocolate", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "the-real-deal-for-supermarket-savvy-shopping", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "the-umami-guide-to-our-favourite-mushrooms", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "lets-hear-it-for-carrots", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "baking-mishaps-our-troubleshooting-tips", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "skip-the-spirits-with-delicious-mocktails", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "give-your-oatmeal-the-ultimate-makeover", + ], + }, + }, +] +`; + +exports[`translatePathFromContext() translates a path 1`] = ` +{ + "entity": { + "bundle": "recipe", + "canonical": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", + "id": "1", + "langcode": "en", + "path": "/en/recipes/deep-mediterranean-quiche", + "type": "node", + "uuid": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + }, + "isHomePath": false, + "jsonapi": { + "basePath": "/en/jsonapi", + "entryPoint": "https://tests.next-drupal.org/en/jsonapi", + "individual": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "pathPrefix": "en/jsonapi", + "resourceName": "node--recipe", + }, + "label": "Deep mediterranean quiche - edited", + "meta": { + "deprecated": { + "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.", + }, + }, + "resolved": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", +} +`; + +exports[`translatePathFromContext() translates a path with pathPrefix 1`] = ` +{ + "entity": { + "bundle": "recipe", + "canonical": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", + "id": "1", + "langcode": "en", + "path": "/en/recipes/deep-mediterranean-quiche", + "type": "node", + "uuid": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + }, + "isHomePath": false, + "jsonapi": { + "basePath": "/en/jsonapi", + "entryPoint": "https://tests.next-drupal.org/en/jsonapi", + "individual": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "pathPrefix": "en/jsonapi", + "resourceName": "node--recipe", + }, + "label": "Deep mediterranean quiche - edited", + "meta": { + "deprecated": { + "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.", + }, + }, + "resolved": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", +} +`; + +exports[`getPathFromContext() encodes path with punctuation 1`] = ` +{ + "entity": { + "bundle": "page", + "canonical": "https://tests.next-drupal.org/en/path%26with%5Epunc%26in%24path", + "id": "20", + "langcode": "en", + "path": "/en/path%26with%5Epunc%26in%24path", + "type": "node", + "uuid": "e4bbe727-14ee-44bc-a34a-14ce5c86a82e", + }, + "isHomePath": false, + "jsonapi": { + "basePath": "/en/jsonapi", + "entryPoint": "https://tests.next-drupal.org/en/jsonapi", + "individual": "https://tests.next-drupal.org/en/jsonapi/node/page/e4bbe727-14ee-44bc-a34a-14ce5c86a82e", + "pathPrefix": "en/jsonapi", + "resourceName": "node--page", + }, + "label": "Page with characters in path", + "meta": { + "deprecated": { + "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.", + }, + }, + "resolved": "https://tests.next-drupal.org/en/path%26with%5Epunc%26in%24path", +} +`; + +exports[`getResourceCollectionFromContext() fetches a resource collection 1`] = ` +[ + { + "id": "52837ad0-f218-46bd-a106-5710336b7053", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", + }, + }, + "title": "Give it a go and grow your own herbs", + "type": "node--article", + }, + { + "id": "3d5a7bca-5b4a-49ee-87d2-cbe313562903", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/3d5a7bca-5b4a-49ee-87d2-cbe313562903?resourceVersion=id%3A22", + }, + }, + "title": "Dairy-free and delicious milk chocolate", + "type": "node--article", + }, + { + "id": "a1ef61c9-ed15-4b6d-bba2-bf6e5141f961", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/a1ef61c9-ed15-4b6d-bba2-bf6e5141f961?resourceVersion=id%3A24", + }, + }, + "title": "The real deal for supermarket savvy shopping", + "type": "node--article", + }, + { + "id": "ac0b8b56-db8d-4322-9b1e-224f22083f0d", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ac0b8b56-db8d-4322-9b1e-224f22083f0d?resourceVersion=id%3A26", + }, + }, + "title": "The Umami guide to our favorite mushrooms", + "type": "node--article", + }, + { + "id": "22874ecc-0443-441b-a3c9-3aa94d85b800", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/22874ecc-0443-441b-a3c9-3aa94d85b800?resourceVersion=id%3A28", + }, + }, + "title": "Let's hear it for carrots", + "type": "node--article", + }, + { + "id": "ec51b1de-f51e-4761-b061-aa2624e58b4a", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/ec51b1de-f51e-4761-b061-aa2624e58b4a?resourceVersion=id%3A30", + }, + }, + "title": "Baking mishaps - our troubleshooting tips", + "type": "node--article", + }, + { + "id": "d5b9ef80-c0aa-45ca-88e7-79c28abe5d50", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/d5b9ef80-c0aa-45ca-88e7-79c28abe5d50?resourceVersion=id%3A32", + }, + }, + "title": "Skip the spirits with delicious mocktails", + "type": "node--article", + }, + { + "id": "6c7c249f-cd1a-41d5-944b-b3bfce68b325", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/article/6c7c249f-cd1a-41d5-944b-b3bfce68b325?resourceVersion=id%3A34", + }, + }, + "title": "Give your oatmeal the ultimate makeover", + "type": "node--article", + }, +] +`; + +exports[`getResourceCollectionFromContext() fetches a resource collection using locale 1`] = ` +[ + { + "id": "52837ad0-f218-46bd-a106-5710336b7053", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053?resourceVersion=id%3A20", + }, + }, + "title": "Prueba y cultiva tus propias hierbas", + "type": "node--article", + }, + { + "id": "3d5a7bca-5b4a-49ee-87d2-cbe313562903", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/3d5a7bca-5b4a-49ee-87d2-cbe313562903?resourceVersion=id%3A22", + }, + }, + "title": "Delicioso chocolate sin lactosa", + "type": "node--article", + }, + { + "id": "a1ef61c9-ed15-4b6d-bba2-bf6e5141f961", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/a1ef61c9-ed15-4b6d-bba2-bf6e5141f961?resourceVersion=id%3A24", + }, + }, + "title": "El verdadero negocio para comprar en el supermercado", + "type": "node--article", + }, + { + "id": "ac0b8b56-db8d-4322-9b1e-224f22083f0d", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/ac0b8b56-db8d-4322-9b1e-224f22083f0d?resourceVersion=id%3A26", + }, + }, + "title": "Guía Umami de nuestras setas preferidas", + "type": "node--article", + }, + { + "id": "22874ecc-0443-441b-a3c9-3aa94d85b800", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/22874ecc-0443-441b-a3c9-3aa94d85b800?resourceVersion=id%3A28", + }, + }, + "title": "Un aplauso para las zanahorias", + "type": "node--article", + }, + { + "id": "ec51b1de-f51e-4761-b061-aa2624e58b4a", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/ec51b1de-f51e-4761-b061-aa2624e58b4a?resourceVersion=id%3A30", + }, + }, + "title": "Percances al hornear - nuestros consejos para solucionar los problemas", + "type": "node--article", + }, + { + "id": "d5b9ef80-c0aa-45ca-88e7-79c28abe5d50", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/d5b9ef80-c0aa-45ca-88e7-79c28abe5d50?resourceVersion=id%3A32", + }, + }, + "title": "Salta los espíritus con deliciosos cócteles sin alcohol", + "type": "node--article", + }, + { + "id": "6c7c249f-cd1a-41d5-944b-b3bfce68b325", + "langcode": "es", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/article/6c7c249f-cd1a-41d5-944b-b3bfce68b325?resourceVersion=id%3A34", + }, + }, + "title": "Dale a tu avena el cambio de imagen definitivo", + "type": "node--article", + }, +] +`; + +exports[`getResourceCollectionFromContext() fetches raw data 1`] = ` +{ + "data": [ + { + "attributes": { + "title": "Vegan chocolate and nut brownies", + }, + "id": "7862d283-ece2-4ae1-8bef-865259f72315", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/7862d283-ece2-4ae1-8bef-865259f72315?resourceVersion=id%3A4", + }, + }, + "type": "node--recipe", + }, + { + "attributes": { + "title": "Super easy vegetarian pasta bake", + }, + "id": "8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/8d4b237f-f6fe-4bb1-95ef-f9e3b3f9d6b7?resourceVersion=id%3A6", + }, + }, + "type": "node--recipe", + }, + ], + "jsonapi": { + "meta": { + "links": { + "self": { + "href": "http://jsonapi.org/format/1.0/", + }, + }, + }, + "version": "1.0", + }, + "links": { + "next": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe?fields%5Bnode--recipe%5D=title&page%5Boffset%5D=2&page%5Blimit%5D=2", + }, + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe?fields%5Bnode--recipe%5D=title&page%5Blimit%5D=2", + }, + }, +} +`; + +exports[`getResourceFromContext() accepts a translated path 1`] = ` +{ + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "path": { + "alias": "/recipes/deep-mediterranean-quiche", + "langcode": "en", + "pid": 67, + }, + "status": true, + "title": "Deep mediterranean quiche - edited", + "type": "node--recipe", +} +`; + +exports[`getResourceFromContext() fetches a resource from context 1`] = ` +{ + "changed": "2022-03-25T08:02:17+00:00", + "content_translation_outdated": false, + "content_translation_source": "und", + "created": "2022-03-21T10:52:42+00:00", + "default_langcode": true, + "drupal_internal__nid": 1, + "drupal_internal__vid": 37, + "field_cooking_time": 30, + "field_difficulty": "medium", + "field_ingredients": [ + "For the pastry:", + "280g plain flour", + "140g butter", + "Cold water", + "For the filling:", + "1 onion", + "2 garlic cloves", + "Half a courgette", + "450ml soya milk", + "500g grated parmesan", + "2 eggs", + "200g sun dried tomatoes", + "100g feta", + ], + "field_media_image": { + "id": "bbfe9d97-2da2-432b-a22c-0396c08e06ca", + "resourceIdObjMeta": { + "drupal_internal__target_id": 1, + }, + "type": "media--image", + }, + "field_number_of_servings": 8, + "field_preparation_time": 40, + "field_recipe_category": [ + { + "id": "a6c02fe4-67bf-462c-90cb-32281a07efe4", + "resourceIdObjMeta": { + "drupal_internal__target_id": 31, + }, + "type": "taxonomy_term--recipe_category", + }, + ], + "field_recipe_instruction": { + "format": "basic_html", + "processed": "
  1. Preheat the oven to 400°F/200°C. Starting with the pastry; rub the flour and butter together in a bowl until crumbling like breadcrumbs. Add water, a little at a time, until it forms a dough.
  2. +
  3. Roll out the pastry on a floured board and gently spread over your tin. Place in the fridge for 20 minutes before blind baking for a further 10.
  4. +
  5. Whilst the pastry is cooling, chop and gently cook the onions, garlic and courgette.
  6. +
  7. In a large bowl, add the soya milk, half the parmesan, and the eggs. Gently mix.
  8. +
  9. Once the pastry is cooked, spread the onions, garlic and sun dried tomatoes over the base and pour the eggs mix over. Sprinkle the remaining parmesan and careful lay the feta over the top. Bake for 30 minutes or until golden brown.
  10. +
", + "value": "
    +
  1. Preheat the oven to 400°F/200°C. Starting with the pastry; rub the flour and butter together in a bowl until crumbling like breadcrumbs. Add water, a little at a time, until it forms a dough.
  2. +
  3. Roll out the pastry on a floured board and gently spread over your tin. Place in the fridge for 20 minutes before blind baking for a further 10.
  4. +
  5. Whilst the pastry is cooling, chop and gently cook the onions, garlic and courgette.
  6. +
  7. In a large bowl, add the soya milk, half the parmesan, and the eggs. Gently mix.
  8. +
  9. Once the pastry is cooked, spread the onions, garlic and sun dried tomatoes over the base and pour the eggs mix over. Sprinkle the remaining parmesan and careful lay the feta over the top. Bake for 30 minutes or until golden brown.
  10. +
+", + }, + "field_summary": { + "format": "basic_html", + "processed": "

An Italian inspired quiche with sun dried tomatoes and courgette. A perfect light meal for a summer's day.

+", + "value": "

An Italian inspired quiche with sun dried tomatoes and courgette. A perfect light meal for a summer's day.

+", + }, + "field_tags": [ + { + "id": "46258827-cfad-4813-99dc-287c4cb41117", + "resourceIdObjMeta": { + "drupal_internal__target_id": 22, + }, + "type": "taxonomy_term--tags", + }, + { + "id": "f32a4d84-0568-4bfd-8be3-8217d36efb6d", + "resourceIdObjMeta": { + "drupal_internal__target_id": 13, + }, + "type": "taxonomy_term--tags", + }, + ], + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "langcode": "en", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "moderation_state": "published", + "node_type": { + "id": "9b70a287-cade-454f-be8b-dea7b9a37c7a", + "resourceIdObjMeta": { + "drupal_internal__target_id": "recipe", + }, + "type": "node_type--node_type", + }, + "path": { + "alias": "/recipes/deep-mediterranean-quiche", + "langcode": "en", + "pid": 67, + }, + "promote": true, + "relationshipNames": [ + "node_type", + "revision_uid", + "uid", + "field_media_image", + "field_recipe_category", + "field_tags", + ], + "revision_log": null, + "revision_timestamp": "2022-03-25T08:02:17+00:00", + "revision_translation_affected": true, + "revision_uid": { + "id": "365cc7b5-ddc4-4b3b-939e-1494400aab4a", + "resourceIdObjMeta": { + "drupal_internal__target_id": 1, + }, + "type": "user--user", + }, + "status": true, + "sticky": false, + "title": "Deep mediterranean quiche - edited", + "type": "node--recipe", + "uid": { + "id": "9e4944e8-dd77-407a-8610-83e823b48b56", + "resourceIdObjMeta": { + "drupal_internal__target_id": 4, + }, + "type": "user--user", + }, +} +`; + +exports[`getResourceFromContext() fetches a resource from context using locale 1`] = ` +{ + "field_cooking_time": 30, + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "links": { + "self": { + "href": "https://tests.next-drupal.org/es/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "title": "Quiche mediterráneo profundo", + "type": "node--recipe", +} +`; + +exports[`getResourceFromContext() fetches a resource from context with params 1`] = ` +{ + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "title": "Deep mediterranean quiche - edited", + "type": "node--recipe", +} +`; + +exports[`getResourceFromContext() fetches raw data 1`] = ` +{ + "data": { + "attributes": { + "title": "Deep mediterranean quiche - edited", + }, + "id": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?resourceVersion=id%3A37", + }, + }, + "type": "node--recipe", + }, + "jsonapi": { + "meta": { + "links": { + "self": { + "href": "http://jsonapi.org/format/1.0/", + }, + }, + }, + "version": "1.0", + }, + "links": { + "self": { + "href": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f?fields%5Bnode--recipe%5D=title&resourceVersion=rel%3Alatest-version", + }, + }, +} +`; + +exports[`getStaticPathsFromContext() returns static paths for multiple resource types from context 1`] = ` +[ + { + "locale": "en", + "params": { + "slug": [ + "articles", + "give-it-a-go-and-grow-your-own-herbs", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "dairy-free-and-delicious-milk-chocolate", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "the-real-deal-for-supermarket-savvy-shopping", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "the-umami-guide-to-our-favourite-mushrooms", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "lets-hear-it-for-carrots", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "baking-mishaps-our-troubleshooting-tips", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "skip-the-spirits-with-delicious-mocktails", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "give-your-oatmeal-the-ultimate-makeover", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "prueba-y-cultiva-tus-propias-hierbas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "delicioso-chocolate-sin-lactosa", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "el-verdadeo-negocio-para-comprar-en-el-supermercado", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "guia-umami-de-nuestras-setas-preferidas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "un-aplauso-para-las-zanahorias", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "percances-al-hornear-nuestros-consejos-para-solucionar-problemas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "salta-los-espiritus-con-deliciosos-cocteles-sin-alcohol", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "dale-a-tu-avena-el-cambio-de-imagen-definitivo", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "vegan-chocolate-and-nut-brownies", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "super-easy-vegetarian-pasta-bake", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "watercress-soup", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "victoria-sponge-cake", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "gluten-free-pizza", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "thai-green-curry", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "crema-catalana", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "fiery-chili-sauce", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "recipes", + "deep-mediterranean-quiche", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "bizcochos-veganos-de-chocolate-y-nueces", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "pasta-vegetariana-horno-super-facil", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "sopa-de-berro", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "pastel-victoria", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "pizza-sin-gluten", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "curry-verde-tailandes", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "crema-catalana", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "salsa-de-chile-ardiente", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "recipes", + "quiche-mediterráneo-profundo", + ], + }, + }, +] +`; + +exports[`getStaticPathsFromContext() returns static paths from context 1`] = ` +[ + { + "params": { + "slug": [ + "articles", + "give-it-a-go-and-grow-your-own-herbs", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "dairy-free-and-delicious-milk-chocolate", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "the-real-deal-for-supermarket-savvy-shopping", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "the-umami-guide-to-our-favourite-mushrooms", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "lets-hear-it-for-carrots", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "baking-mishaps-our-troubleshooting-tips", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "skip-the-spirits-with-delicious-mocktails", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "give-your-oatmeal-the-ultimate-makeover", + ], + }, + }, +] +`; + +exports[`getStaticPathsFromContext() returns static paths from context with locale 1`] = ` +[ + { + "locale": "en", + "params": { + "slug": [ + "articles", + "give-it-a-go-and-grow-your-own-herbs", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "dairy-free-and-delicious-milk-chocolate", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "the-real-deal-for-supermarket-savvy-shopping", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "the-umami-guide-to-our-favourite-mushrooms", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "lets-hear-it-for-carrots", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "baking-mishaps-our-troubleshooting-tips", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "skip-the-spirits-with-delicious-mocktails", + ], + }, + }, + { + "locale": "en", + "params": { + "slug": [ + "articles", + "give-your-oatmeal-the-ultimate-makeover", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "prueba-y-cultiva-tus-propias-hierbas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "delicioso-chocolate-sin-lactosa", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "el-verdadeo-negocio-para-comprar-en-el-supermercado", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "guia-umami-de-nuestras-setas-preferidas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "un-aplauso-para-las-zanahorias", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "percances-al-hornear-nuestros-consejos-para-solucionar-problemas", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "salta-los-espiritus-con-deliciosos-cocteles-sin-alcohol", + ], + }, + }, + { + "locale": "es", + "params": { + "slug": [ + "articles", + "dale-a-tu-avena-el-cambio-de-imagen-definitivo", + ], + }, + }, +] +`; + +exports[`getStaticPathsFromContext() returns static paths from context with params 1`] = ` +[ + { + "params": { + "slug": [ + "articles", + "give-it-a-go-and-grow-your-own-herbs", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "dairy-free-and-delicious-milk-chocolate", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "the-real-deal-for-supermarket-savvy-shopping", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "the-umami-guide-to-our-favourite-mushrooms", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "lets-hear-it-for-carrots", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "baking-mishaps-our-troubleshooting-tips", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "skip-the-spirits-with-delicious-mocktails", + ], + }, + }, + { + "params": { + "slug": [ + "articles", + "give-your-oatmeal-the-ultimate-makeover", + ], + }, + }, +] +`; + +exports[`translatePathFromContext() translates a path 1`] = ` +{ + "entity": { + "bundle": "recipe", + "canonical": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", + "id": "1", + "langcode": "en", + "path": "/en/recipes/deep-mediterranean-quiche", + "type": "node", + "uuid": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + }, + "isHomePath": false, + "jsonapi": { + "basePath": "/en/jsonapi", + "entryPoint": "https://tests.next-drupal.org/en/jsonapi", + "individual": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "pathPrefix": "en/jsonapi", + "resourceName": "node--recipe", + }, + "label": "Deep mediterranean quiche - edited", + "meta": { + "deprecated": { + "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.", + }, + }, + "resolved": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", +} +`; + +exports[`translatePathFromContext() translates a path with pathPrefix 1`] = ` +{ + "entity": { + "bundle": "recipe", + "canonical": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", + "id": "1", + "langcode": "en", + "path": "/en/recipes/deep-mediterranean-quiche", + "type": "node", + "uuid": "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + }, + "isHomePath": false, + "jsonapi": { + "basePath": "/en/jsonapi", + "entryPoint": "https://tests.next-drupal.org/en/jsonapi", + "individual": "https://tests.next-drupal.org/en/jsonapi/node/recipe/71e04ead-4cc7-416c-b9ca-60b635fdc50f", + "pathPrefix": "en/jsonapi", + "resourceName": "node--recipe", + }, + "label": "Deep mediterranean quiche - edited", + "meta": { + "deprecated": { + "jsonapi.pathPrefix": "This property has been deprecated and will be removed in the next version of Decoupled Router. Use basePath instead.", + }, + }, + "resolved": "https://tests.next-drupal.org/en/recipes/deep-mediterranean-quiche", +} +`; diff --git a/packages/next-drupal/tests/DrupalClient/basic-methods.test.ts b/packages/next-drupal/tests/DrupalClient/basic-methods.test.ts new file mode 100644 index 00000000..c8684a47 --- /dev/null +++ b/packages/next-drupal/tests/DrupalClient/basic-methods.test.ts @@ -0,0 +1,597 @@ +import { afterEach, describe, expect, jest, test } from "@jest/globals" +import { NextApiRequest, NextApiResponse } from "next" +import { DRAFT_DATA_COOKIE_NAME, DrupalClient, JsonApiErrors } from "../../src" +import { BASE_URL, mockLogger, spyOnFetch, spyOnFetchOnce } from "../utils" +import type { DrupalNode, JsonApiError, Serializer } from "../../src" + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe("buildMenuTree()", () => { + test.todo("add tests") +}) + +describe("buildUrl()", () => { + const client = new DrupalClient(BASE_URL) + + test("builds a url", () => { + expect(client.buildUrl("http://example.com").toString()).toEqual( + "http://example.com/" + ) + }) + + test("builds a relative url", () => { + expect(client.buildUrl("/foo").toString()).toEqual(`${BASE_URL}/foo`) + }) + + test("builds a url with params", () => { + expect(client.buildUrl("/foo", { bar: "baz" }).toString()).toEqual( + `${BASE_URL}/foo?bar=baz` + ) + + expect( + client + .buildUrl("/jsonapi/node/article", { + sort: "-created", + "fields[node--article]": "title,path", + }) + .toString() + ).toEqual( + `${BASE_URL}/jsonapi/node/article?sort=-created&fields%5Bnode--article%5D=title%2Cpath` + ) + }) + + test("builds a url from object (DrupalJsonApiParams)", () => { + const params = { + getQueryObject: () => ({ + sort: "-created", + "fields[node--article]": "title,path", + }), + } + + expect(client.buildUrl("/jsonapi/node/article", params).toString()).toEqual( + `${BASE_URL}/jsonapi/node/article?sort=-created&fields%5Bnode--article%5D=title%2Cpath` + ) + }) +}) + +describe("debug()", () => { + test("does not print messages by default", () => { + const logger = mockLogger() + const client = new DrupalClient(BASE_URL, { logger }) + const message = "Example message" + client.debug(message) + expect(logger.debug).not.toHaveBeenCalled() + }) + + test("prints messages when debugging on", () => { + const logger = mockLogger() + const client = new DrupalClient(BASE_URL, { logger, debug: true }) + const message = "Example message" + client.debug(message) + expect(logger.debug).toHaveBeenCalledWith("Debug mode is on.") + expect(logger.debug).toHaveBeenCalledWith(message) + }) +}) + +describe("deserialize()", () => { + test("deserializes JSON:API resource", async () => { + const client = new DrupalClient(BASE_URL) + const url = client.buildUrl( + "/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053", + { + include: "field_tags", + } + ) + + const response = await client.fetch(url.toString()) + const json = await response.json() + const article = client.deserialize(json) as DrupalNode + + expect(article).toMatchSnapshot() + expect(article.id).toEqual("52837ad0-f218-46bd-a106-5710336b7053") + expect(article.field_tags).toHaveLength(3) + }) + + test("deserializes JSON:API collection", async () => { + const client = new DrupalClient(BASE_URL) + const url = client.buildUrl("/jsonapi/node/article", { + getQueryObject: () => ({ + "fields[node--article]": "title", + }), + }) + + const response = await client.fetch(url.toString()) + const json = await response.json() + const articles = client.deserialize(json) as DrupalNode[] + + expect(articles).toMatchSnapshot() + }) + + test("allows for custom data serializer", async () => { + const serializer: Serializer = { + deserialize: ( + body: { data: { id: string; attributes: { title: string } } }, + options: { pathPrefix: string } + ) => { + return { + id: body.data.id, + title: `${options.pathPrefix}: ${body.data.attributes.title}`, + } + }, + } + const client = new DrupalClient(BASE_URL, { + serializer, + }) + const url = client.buildUrl( + "/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053" + ) + + const response = await client.fetch(url.toString()) + const json = await response.json() + const article = client.deserialize(json, { + pathPrefix: "TITLE", + }) as DrupalNode + + expect(article).toMatchSnapshot() + expect(article.id).toEqual("52837ad0-f218-46bd-a106-5710336b7053") + expect(article.title).toEqual(`TITLE: ${json.data.attributes.title}`) + }) + + test("returns null if no body", () => { + const client = new DrupalClient(BASE_URL) + expect(client.deserialize("")).toBe(null) + }) +}) + +describe("formatJsonApiErrors()", () => { + const errors: JsonApiError[] = [ + { + status: "404", + title: "First error", + }, + { + status: "500", + title: "Second error", + detail: "is ignored", + }, + ] + const client = new DrupalClient(BASE_URL) + + test("formats the first error in the array", () => { + expect(client.formatJsonApiErrors(errors)).toBe("404 First error") + }) + + test("includes the optional error detail", () => { + expect( + client.formatJsonApiErrors([ + { + ...errors[0], + detail: "Detail is included.", + }, + errors[1], + ]) + ).toBe("404 First error\nDetail is included.") + }) +}) + +describe("getErrorsFromResponse()", () => { + const client = new DrupalClient(BASE_URL) + + test("returns application/json error message", async () => { + const message = "An error occurred." + const response = new Response(JSON.stringify({ message }), { + status: 403, + headers: { + "content-type": "application/json", + }, + }) + + expect(await client.getErrorsFromResponse(response)).toBe(message) + }) + + test("returns application/vnd.api+json errors", async () => { + const payload = { + errors: [ + { + status: "404", + title: "Not found", + detail: "Oops.", + }, + { + status: "418", + title: "I am a teapot", + detail: "Even RFCs have easter eggs.", + }, + ] as JsonApiError[], + } + const response = new Response(JSON.stringify(payload), { + status: 403, + headers: { + "content-type": "application/vnd.api+json", + }, + }) + + expect(await client.getErrorsFromResponse(response)).toMatchObject( + payload.errors + ) + }) + + test("returns the response status text if the application/vnd.api+json errors cannot be found", async () => { + const payload = { + contains: 'no "errors" entry', + } + const response = new Response(JSON.stringify(payload), { + status: 418, + statusText: "I'm a Teapot", + headers: { + "content-type": "application/vnd.api+json", + }, + }) + + expect(await client.getErrorsFromResponse(response)).toBe("I'm a Teapot") + }) + + test("returns the response status text if no errors can be found", async () => { + const response = new Response(JSON.stringify({}), { + status: 403, + statusText: "Forbidden", + }) + + expect(await client.getErrorsFromResponse(response)).toBe("Forbidden") + }) +}) + +describe("preview()", () => { + // Get values from our mocked request. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { slug, resourceVersion, plugin, secret, ...draftData } = + new NextApiRequest().query + const dataCookie = `${DRAFT_DATA_COOKIE_NAME}=${encodeURIComponent( + JSON.stringify({ slug, resourceVersion, ...draftData }) + )}; Path=/; HttpOnly; SameSite=None; Secure` + const validationPayload = { + slug, + maxAge: 30, + } + + test("turns on preview mode and clears preview data", async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const client = new DrupalClient(BASE_URL) + spyOnFetch({ responseBody: validationPayload }) + + await client.preview(request, response) + + expect(response.clearPreviewData).toBeCalledTimes(1) + expect(response.setPreviewData).toBeCalledWith({ + resourceVersion, + plugin, + ...validationPayload, + }) + }) + + test("does not enable preview mode if validation fails", async () => { + const logger = mockLogger() + const request = new NextApiRequest() + const response = new NextApiResponse() + const client = new DrupalClient(BASE_URL, { debug: true, logger }) + const status = 403 + const message = "mock fail" + spyOnFetch({ + responseBody: { message }, + status, + headers: { + "Content-Type": "application/json", + }, + }) + + await client.preview(request, response) + + expect(logger.debug).toBeCalledWith( + `Draft url validation error: ${message}` + ) + expect(response.setPreviewData).toBeCalledTimes(0) + expect(response.statusCode).toBe(status) + expect(response.json).toBeCalledWith({ message }) + }) + + test("does not turn on draft mode by default", async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const client = new DrupalClient(BASE_URL) + spyOnFetch({ responseBody: validationPayload }) + + await client.preview(request, response) + + expect(response.setDraftMode).toBeCalledTimes(0) + + // Also check for no draft data cookie. + const cookies = response.getHeader("Set-Cookie") + expect(cookies[cookies.length - 1]).not.toBe(dataCookie) + }) + + test("optionally turns on draft mode", async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const logger = mockLogger() + const client = new DrupalClient(BASE_URL, { + debug: true, + logger, + }) + spyOnFetch({ responseBody: validationPayload }) + + const options = { enable: true } + await client.preview(request, response, options) + + expect(response.setDraftMode).toBeCalledWith(options) + + // Also check for draft data cookie. + const cookies = response.getHeader("Set-Cookie") + expect(cookies[cookies.length - 1]).toBe(dataCookie) + + expect(logger.debug).toHaveBeenLastCalledWith("Draft mode enabled.") + }) + + test("updates preview mode cookie’s sameSite flag", async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const client = new DrupalClient(BASE_URL) + spyOnFetch({ responseBody: validationPayload }) + + // Our mock response.setPreviewData() does not set a cookie, so we set one. + const previewCookie = + "__next_preview_data=secret-data; Path=/; HttpOnly; SameSite=Lax" + response.setHeader("Set-Cookie", [ + previewCookie, + ...response.getHeader("Set-Cookie"), + ]) + + const cookies = response.getHeader("Set-Cookie") + cookies[0] = cookies[0].replace("SameSite=Lax", "SameSite=None; Secure") + + await client.preview(request, response) + + expect(response.getHeader).toHaveBeenLastCalledWith("Set-Cookie") + expect(response.setHeader).toHaveBeenLastCalledWith("Set-Cookie", cookies) + expect(response.getHeader("Set-Cookie")).toStrictEqual(cookies) + }) + + test("redirects to the slug path", async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const logger = mockLogger() + const client = new DrupalClient(BASE_URL, { debug: true, logger }) + spyOnFetch({ responseBody: validationPayload }) + + await client.preview(request, response) + + expect(response.setPreviewData).toBeCalledWith({ + resourceVersion, + plugin, + ...validationPayload, + }) + expect(response.writeHead).toBeCalledWith(307, { Location: slug }) + expect(logger.debug).toHaveBeenLastCalledWith("Preview mode enabled.") + }) + + test("returns a 422 response on error", async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const logger = mockLogger() + const client = new DrupalClient(BASE_URL, { debug: true, logger }) + const message = "mock internal error" + response.clearPreviewData = jest.fn(() => { + throw new Error(message) + }) + + await client.preview(request, response) + + expect(logger.debug).toHaveBeenLastCalledWith(`Preview failed: ${message}`) + expect(response.status).toBeCalledWith(422) + expect(response.end).toHaveBeenCalled() + }) +}) + +describe("previewDisable()", () => { + test("clears preview data", async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const client = new DrupalClient(BASE_URL) + + await client.previewDisable(request, response) + expect(response.clearPreviewData).toBeCalledTimes(1) + }) + + test("disables draft mode", async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const client = new DrupalClient(BASE_URL) + + await client.previewDisable(request, response) + expect(response.setDraftMode).toBeCalledWith({ enable: false }) + }) + + test("deletes the draft cookie", async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const client = new DrupalClient(BASE_URL) + + await client.previewDisable(request, response) + const cookies = response.getHeader("Set-Cookie") + expect(cookies[cookies.length - 1]).toBe( + `${DRAFT_DATA_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=None; Secure` + ) + }) + + test('redirects to "/"', async () => { + const request = new NextApiRequest() + const response = new NextApiResponse() + const client = new DrupalClient(BASE_URL) + + await client.previewDisable(request, response) + expect(response.writeHead).toBeCalledWith(307, { Location: "/" }) + expect(response.end).toBeCalled() + }) +}) + +describe("throwError()", () => { + test("throws the error", () => { + const client = new DrupalClient(BASE_URL) + expect(() => { + client.throwError(new Error("Example error")) + }).toThrow("Example error") + }) + + test("logs the error when throwJsonApiErrors is false", () => { + const logger = mockLogger() + const client = new DrupalClient(BASE_URL, { + throwJsonApiErrors: false, + logger, + }) + expect(() => { + client.throwError(new Error("Example error")) + }).not.toThrow() + expect(logger.error).toHaveBeenCalledWith(new Error("Example error")) + }) +}) + +describe("throwIfJsonApiErrors()", () => { + const client = new DrupalClient(BASE_URL) + + test("does not throw if response is ok", async () => { + expect.assertions(1) + + const response = new Response(JSON.stringify({})) + + await expect(client.throwIfJsonApiErrors(response)).resolves.toBe(undefined) + }) + + test("throws a JsonApiErrors object", async () => { + expect.assertions(1) + + const payload = { + errors: [ + { + status: "404", + title: "Not found", + detail: "Oops.", + }, + { + status: "418", + title: "I am a teapot", + detail: "Even RFCs have easter eggs.", + }, + ] as JsonApiError[], + } + const status = 403 + const response = new Response(JSON.stringify(payload), { + status, + headers: { + "content-type": "application/vnd.api+json", + }, + }) + + const expectedError = new JsonApiErrors(payload.errors, status) + await expect(client.throwIfJsonApiErrors(response)).rejects.toEqual( + expectedError + ) + }) +}) + +describe("validateDraftUrl()", () => { + test("outputs debug messages", async () => { + const logger = mockLogger() + const client = new DrupalClient(BASE_URL, { + debug: true, + logger, + }) + const slug = "/example" + const searchParams = new URLSearchParams({ + slug, + }) + + const testPayload = { test: "resolved" } + spyOnFetchOnce({ + responseBody: testPayload, + }) + spyOnFetchOnce({ + responseBody: { + message: "fail", + }, + status: 404, + }) + + let response = await client.validateDraftUrl(searchParams) + expect(response.status).toBe(200) + expect(logger.debug).toHaveBeenCalledWith("Debug mode is on.") + expect(logger.debug).toHaveBeenCalledWith( + `Fetching draft url validation for ${slug}.` + ) + expect(logger.debug).toHaveBeenCalledWith(`Validated slug, ${slug}`) + + response = await client.validateDraftUrl(searchParams) + expect(response.status).toBe(404) + expect(logger.debug).toHaveBeenCalledWith( + `Could not validate slug, ${slug}` + ) + }) + + test("calls draft-url endpoint", async () => { + const client = new DrupalClient(BASE_URL) + const searchParams = new URLSearchParams({ + slug: "/example", + }) + + const testPayload = { test: "resolved" } + const fetchSpy = spyOnFetch({ responseBody: testPayload }) + + await client.validateDraftUrl(searchParams) + + expect(fetchSpy).toHaveBeenNthCalledWith( + 1, + `${BASE_URL}/next/draft-url`, + expect.objectContaining({ + method: "POST", + headers: { + Accept: "application/vnd.api+json", + "Content-Type": "application/json", + }, + body: JSON.stringify(Object.fromEntries(searchParams.entries())), + }) + ) + }) + + test("returns a response object on success", async () => { + const client = new DrupalClient(BASE_URL) + const searchParams = new URLSearchParams({ + slug: "/example", + }) + + const testPayload = { test: "resolved" } + spyOnFetch({ responseBody: testPayload }) + + const response = await client.validateDraftUrl(searchParams) + + expect(response.ok).toBe(true) + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject(testPayload) + }) + + test("returns a response if fetch throws", async () => { + const client = new DrupalClient(BASE_URL) + const searchParams = new URLSearchParams({ + slug: "/example", + }) + + const message = "random fetch error" + spyOnFetch({ throwErrorMessage: message }) + + const response = await client.validateDraftUrl(searchParams) + + expect(response.ok).toBe(false) + expect(response.status).toBe(401) + expect(await response.json()).toMatchObject({ message }) + }) +}) diff --git a/packages/next-drupal/tests/DrupalClient/constructor.test.ts b/packages/next-drupal/tests/DrupalClient/constructor.test.ts new file mode 100644 index 00000000..b31dba4e --- /dev/null +++ b/packages/next-drupal/tests/DrupalClient/constructor.test.ts @@ -0,0 +1,333 @@ +import { afterEach, describe, expect, jest, test } from "@jest/globals" +import { Jsona } from "jsona" +import { DrupalClient } from "../../src" +import { DEBUG_MESSAGE_PREFIX, logger as defaultLogger } from "../../src/logger" +import { BASE_URL } from "../utils" +import type { Logger } from "../../src" + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe("baseUrl parameter", () => { + const env = process.env + + beforeEach(() => { + jest.resetModules() + process.env = { ...env } + }) + + afterEach(() => { + process.env = env + }) + + test("throws error given an invalid baseUrl", () => { + // @ts-ignore + expect(() => new DrupalClient()).toThrow("The 'baseUrl' param is required.") + + // @ts-ignore + expect(() => new DrupalClient({})).toThrow( + "The 'baseUrl' param is required." + ) + }) + + test("turns throwJsonApiErrors off in production", () => { + process.env = { + ...process.env, + NODE_ENV: "production", + } + + const client = new DrupalClient(BASE_URL) + expect(client.throwJsonApiErrors).toBe(false) + }) + + test("announces debug mode when turned on", () => { + const consoleSpy = jest.spyOn(console, "debug").mockImplementation(() => { + // + }) + + new DrupalClient(BASE_URL, { + debug: true, + }) + + expect(consoleSpy).toHaveBeenCalledWith( + DEBUG_MESSAGE_PREFIX, + "Debug mode is on." + ) + }) + + test("returns a DrupalClient", () => { + expect(new DrupalClient(BASE_URL)).toBeInstanceOf(DrupalClient) + }) +}) + +describe("options parameter", () => { + describe("accessToken", () => { + test("defaults to `undefined`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.accessToken).toBe(undefined) + }) + + test("sets the accessToken", async () => { + const accessToken = { + token_type: "Bearer", + expires_in: 300, + access_token: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImVlNDkyOTI4ZTZjNj", + } + + const client = new DrupalClient(BASE_URL, { + accessToken, + }) + + expect(client.accessToken).toEqual(accessToken) + }) + }) + + describe("apiPrefix", () => { + test('defaults to "/jsonapi"', () => { + const client = new DrupalClient(BASE_URL) + expect(client.apiPrefix).toBe("/jsonapi") + }) + + test("sets the apiPrefix", () => { + const customEndPoint = "/customapi" + const client = new DrupalClient(BASE_URL, { + apiPrefix: customEndPoint, + }) + expect(client.apiPrefix).toBe(customEndPoint) + }) + }) + + describe("auth", () => { + test("defaults to `undefined`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.auth).toBe(undefined) + }) + + test("sets the auth credentials", () => { + const auth: DrupalClient["auth"] = { + username: "example", + password: "pw", + } + const client = new DrupalClient(BASE_URL, { + auth, + }) + expect(client._auth).toMatchObject({ + ...auth, + url: "/oauth/token", + }) + }) + }) + + describe("cache", () => { + test("defaults to `null`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.cache).toBe(null) + }) + + test("sets the cache storage", () => { + const customCache: DrupalClient["cache"] = { + async get(key) { + // + }, + async set(key, value, ttl?: number) { + // + }, + } + const client = new DrupalClient(BASE_URL, { + cache: customCache, + }) + expect(client.cache).toBe(customCache) + }) + }) + + describe("debug", () => { + test("defaults to `false`", () => { + const consoleSpy = jest.spyOn(console, "debug").mockImplementation(() => { + // + }) + + new DrupalClient(BASE_URL) + + expect(consoleSpy).toBeCalledTimes(0) + }) + + test("turns on debug mode", () => { + const consoleSpy = jest.spyOn(console, "debug").mockImplementation(() => { + // + }) + + new DrupalClient(BASE_URL, { debug: true }) + + expect(consoleSpy).toBeCalledTimes(1) + }) + }) + + describe("fetcher", () => { + test("defaults to `undefined`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.fetcher).toBe(undefined) + }) + + test("sets up a custom fetcher", () => { + const customFetcher: DrupalClient["fetcher"] = async () => { + // + } + const client = new DrupalClient(BASE_URL, { + fetcher: customFetcher, + }) + expect(client.fetcher).toBe(customFetcher) + }) + }) + + describe("frontPage", () => { + test('defaults to "/home"', () => { + const client = new DrupalClient(BASE_URL) + expect(client.frontPage).toBe("/home") + }) + + test("sets up a custom frontPage", () => { + const customFrontPage = "/front" + + const client = new DrupalClient(BASE_URL, { + frontPage: customFrontPage, + }) + expect(client.frontPage).toBe(customFrontPage) + }) + }) + + describe("headers", () => { + test("defaults to `Content-Type`/`Accept`", () => { + const client = new DrupalClient(BASE_URL) + expect(client._headers).toMatchObject({ + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + }) + }) + + test("sets custom headers", () => { + const customHeaders = { + CustomContentType: "application/json", + CustomAccept: "application/json", + } + + const client = new DrupalClient(BASE_URL, { + headers: customHeaders, + }) + expect(client._headers).toMatchObject(customHeaders) + }) + }) + + describe("logger", () => { + test("defaults to `console`-based `Logger`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.logger).toBe(defaultLogger) + }) + + test("sets up a custom logger", () => { + const customLogger: Logger = { + log: () => { + // + }, + debug: () => { + // + }, + warn: () => { + // + }, + error: () => { + // + }, + } + + const client = new DrupalClient(BASE_URL, { + logger: customLogger, + }) + expect(client.logger).toBe(customLogger) + }) + }) + + describe("previewSecret", () => { + test("defaults to `undefined`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.previewSecret).toBe(undefined) + }) + + test("sets up a custom previewSecret", () => { + const customPreviewSecret = "custom-secret-value" + + const client = new DrupalClient(BASE_URL, { + previewSecret: customPreviewSecret, + }) + expect(client.previewSecret).toBe(customPreviewSecret) + }) + }) + + describe("serializer", () => { + test("defaults to `new Jsona()`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.serializer).toBeInstanceOf(Jsona) + }) + + test("sets up a custom serializer", () => { + const customSerializer: DrupalClient["serializer"] = { + deserialize( + body: Record, + options?: Record + ): unknown { + return { + deserialized: true, + } + }, + } + + const client = new DrupalClient(BASE_URL, { + serializer: customSerializer, + }) + expect(client.serializer).toBe(customSerializer) + }) + }) + + describe("throwJsonApiErrors", () => { + test("defaults to `true`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.throwJsonApiErrors).toBe(true) + }) + + test("can be set to `false`", () => { + const client = new DrupalClient(BASE_URL, { + throwJsonApiErrors: false, + }) + expect(client.throwJsonApiErrors).toBe(false) + }) + }) + + describe("useDefaultResourceTypeEntry", () => { + test("defaults to `false`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.useDefaultResourceTypeEntry).toBe(false) + }) + + test("can be set to `true`", () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + }) + expect(client.useDefaultResourceTypeEntry).toBe(true) + }) + }) + + describe("withAuth", () => { + test("defaults to `false`", () => { + const client = new DrupalClient(BASE_URL) + expect(client.withAuth).toBe(false) + }) + + test("can be set to `true`", () => { + const client = new DrupalClient(BASE_URL, { + withAuth: true, + }) + expect(client.withAuth).toBe(true) + }) + }) +}) diff --git a/packages/next-drupal/tests/crud.test.ts b/packages/next-drupal/tests/DrupalClient/crud-methods.test.ts similarity index 67% rename from packages/next-drupal/tests/crud.test.ts rename to packages/next-drupal/tests/DrupalClient/crud-methods.test.ts index ed9a00b2..cb8e4015 100644 --- a/packages/next-drupal/tests/crud.test.ts +++ b/packages/next-drupal/tests/DrupalClient/crud-methods.test.ts @@ -1,10 +1,25 @@ -import { expect } from "@jest/globals" -import { DrupalClient } from "../src/client" -import type { DrupalNode } from "../src/types" -import { BASE_URL, deleteTestNodes, toggleDrupalModule } from "./utils" +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + jest, + test, +} from "@jest/globals" +import { DrupalClient } from "../../src" +import { + BASE_URL, + deleteTestNodes, + toggleDrupalModule, + mocks, + mockLogger, + spyOnFetch, +} from "../utils" +import type { DrupalNode, JsonApiCreateFileResourceBody } from "../../src" // Enabling and disabling modules takes longer. -// So we increase the time out to handle this. +// So we increase the timeout to 10 seconds to handle this. jest.setTimeout(10000) beforeAll(async () => { @@ -21,8 +36,8 @@ afterAll(async () => { await deleteTestNodes() }) -describe("createResource", () => { - test("it creates a resource", async () => { +describe("createResource()", () => { + test("creates a resource", async () => { const client = new DrupalClient(BASE_URL) const article = await client.createResource( @@ -46,7 +61,7 @@ describe("createResource", () => { expect(article.title).toEqual("TEST New article") }) - test("it creates a resource with a relationship", async () => { + test("creates a resource with a relationship", async () => { const client = new DrupalClient(BASE_URL, { auth: { username: process.env.DRUPAL_USERNAME, @@ -92,7 +107,7 @@ describe("createResource", () => { expect(article.field_media_image.name).toEqual(mediaImage.name) }) - test("it creates a localized resource", async () => { + test("creates a localized resource", async () => { const client = new DrupalClient(BASE_URL, { auth: { username: process.env.DRUPAL_USERNAME, @@ -112,7 +127,7 @@ describe("createResource", () => { expect(article.langcode).toEqual("es") }) - test("it throws an error for missing required attributes", async () => { + test("throws an error for missing required attributes", async () => { const client = new DrupalClient(BASE_URL, { auth: { username: process.env.DRUPAL_USERNAME, @@ -131,7 +146,7 @@ describe("createResource", () => { ) }) - test("it throws an error for invalid attributes", async () => { + test("throws an error for invalid attributes", async () => { const client = new DrupalClient(BASE_URL, { auth: { username: process.env.DRUPAL_USERNAME, @@ -172,8 +187,115 @@ describe("createResource", () => { }) }) -describe("updateResource", () => { - test("it updates a resource", async () => { +describe("createFileResource()", () => { + const mockBody: JsonApiCreateFileResourceBody = { + data: { + attributes: { + type: "file--file", + field: "field_media_image", + filename: "mediterranean-quiche-umami.jpg", + file: Buffer.from("mock-file-data"), + }, + }, + } + const mockResponseData = mocks.resources.file + + test("constructs the API path from body and options", async () => { + const logger = mockLogger() + const client = new DrupalClient("https://example.com", { + useDefaultResourceTypeEntry: true, + debug: true, + logger, + }) + const type = "type--from-first-argument" + const fetchSpy = spyOnFetch({ responseBody: mockResponseData }) + + await client.createFileResource(type, mockBody, { + withAuth: false, + params: { include: "extra_field" }, + }) + + expect(logger.debug).toBeCalledWith( + `Creating file resource for media of type ${type}.` + ) + expect(fetchSpy.mock.lastCall[0]).toBe( + "https://example.com/jsonapi/file/file/field_media_image?include=extra_field" + ) + }) + + test("constructs the API path using non-default locale", async () => { + const client = new DrupalClient("https://example.com", { + useDefaultResourceTypeEntry: true, + }) + const type = "type--from-first-argument" + const fetchSpy = spyOnFetch({ responseBody: mockResponseData }) + + await client.createFileResource(type, mockBody, { + withAuth: false, + params: { include: "extra_field" }, + locale: "es", + defaultLocale: "en", + }) + + expect(fetchSpy.mock.lastCall[0]).toBe( + "https://example.com/es/jsonapi/file/file/field_media_image?include=extra_field" + ) + }) + + test("returns the deserialized data", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + }) + spyOnFetch({ responseBody: mockResponseData }) + + const result = await client.createFileResource("ignored", mockBody, { + withAuth: false, + }) + + expect(result?.filename).toBe(mockResponseData.data.attributes.filename) + expect(result?.data?.attributes?.filename).toBe(undefined) + }) + + test("optionally returns the raw data", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + }) + spyOnFetch({ responseBody: mockResponseData }) + + const result = await client.createFileResource("ignored", mockBody, { + withAuth: false, + deserialize: false, + }) + + expect(result?.filename).toBe(undefined) + expect(result?.data?.attributes?.filename).toBe( + mockResponseData.data.attributes.filename + ) + }) + + test("throws error if response is not ok", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + }) + const message = "mock error" + spyOnFetch({ + responseBody: { message }, + status: 403, + headers: { + "content-type": "application/json", + }, + }) + + await expect( + client.createFileResource("ignored", mockBody, { + withAuth: false, + }) + ).rejects.toThrow(message) + }) +}) + +describe("updateResource()", () => { + test("updates a resource", async () => { const client = new DrupalClient(BASE_URL) const basic = Buffer.from( @@ -213,7 +335,7 @@ describe("updateResource", () => { expect(updatedArticle.title).toEqual("TEST New article updated") }) - test("it updates a resource with a relationship", async () => { + test("updates a resource with a relationship", async () => { const basic = Buffer.from( `${process.env.DRUPAL_USERNAME}:${process.env.DRUPAL_PASSWORD}` ).toString("base64") @@ -273,7 +395,7 @@ describe("updateResource", () => { expect(updatedArticle.field_media_image.name).toEqual(mediaImage.name) }) - test("it throws an error for missing required attributes", async () => { + test("throws an error for missing required attributes", async () => { const client = new DrupalClient(BASE_URL, { auth: { username: process.env.DRUPAL_USERNAME, @@ -302,7 +424,7 @@ describe("updateResource", () => { ) }) - test("it throws an error for invalid attributes", async () => { + test("throws an error for invalid attributes", async () => { const client = new DrupalClient(BASE_URL, { auth: { username: process.env.DRUPAL_USERNAME, @@ -349,8 +471,8 @@ describe("updateResource", () => { }) }) -describe("deleteResource", () => { - test("it deletes a resource", async () => { +describe("deleteResource()", () => { + test("deletes a resource", async () => { const client = new DrupalClient(BASE_URL, { auth: { username: process.env.DRUPAL_USERNAME, @@ -377,7 +499,7 @@ describe("deleteResource", () => { ) }) - test("it throws an error for invalid resource", async () => { + test("throws an error for invalid resource", async () => { const client = new DrupalClient(BASE_URL, { auth: { username: process.env.DRUPAL_USERNAME, diff --git a/packages/next-drupal/tests/DrupalClient/fetch-related-methods.test.ts b/packages/next-drupal/tests/DrupalClient/fetch-related-methods.test.ts new file mode 100644 index 00000000..1bdfe463 --- /dev/null +++ b/packages/next-drupal/tests/DrupalClient/fetch-related-methods.test.ts @@ -0,0 +1,1186 @@ +import { afterEach, describe, expect, jest, test } from "@jest/globals" +import { DrupalClient } from "../../src" +import { + BASE_URL, + mockLogger, + mocks, + spyOnFetch, + spyOnFetchOnce, +} from "../utils" +import type { + AccessToken, + DrupalClientAuth, + DrupalNode, + DrupalSearchApiJsonApiResponse, +} from "../../src" + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe("fetch()", () => { + test("allows fetching custom url", async () => { + const client = new DrupalClient(BASE_URL) + const url = client.buildUrl( + "/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053" + ) + + const response = await client.fetch(url.toString()) + expect(response.headers.get("content-type")).toEqual( + "application/vnd.api+json" + ) + const json = await response.json() + expect(json).toMatchSnapshot() + }) + + test("allows for custom fetcher", async () => { + const customFetch = jest.fn() + + const client = new DrupalClient(BASE_URL, { + fetcher: customFetch, + }) + const url = client.buildUrl("/jsonapi").toString() + + await client.fetch(url) + expect(customFetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + headers: { + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + }, + }) + ) + + await client.fetch(url, { + headers: { + foo: "bar", + }, + }) + expect(customFetch).toHaveBeenLastCalledWith( + url, + expect.objectContaining({ + headers: { + Accept: "application/vnd.api+json", + "Content-Type": "application/vnd.api+json", + foo: "bar", + }, + }) + ) + }) + + describe("authentication", () => { + const clientIdSecret = mocks.auth.clientIdSecret + + test("throws an error if withAuth is called when auth is not configured", async () => { + const client = new DrupalClient(BASE_URL) + + const url = client.buildUrl("/jsonapi") + + await expect( + client.fetch(url.toString(), { + withAuth: true, + }) + ).rejects.toThrow("auth is not configured.") + }) + + test("accepts username and password", async () => { + const customFetch = jest.fn() + + const client = new DrupalClient(BASE_URL, { + auth: { + username: "admin", + password: "password", + }, + fetcher: customFetch, + }) + const url = client.buildUrl("/jsonapi").toString() + + await client.fetch(url, { withAuth: true }) + expect(customFetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + headers: { + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + Authorization: "Basic YWRtaW46cGFzc3dvcmQ=", + }, + withAuth: true, + }) + ) + }) + + test("accepts callback", async () => { + const customAuth = jest + .fn() + .mockReturnValue( + "Basic YXJzaGFkQG5leHQtZHJ1cGFsLm9yZzphYmMxMjM=" + ) as DrupalClientAuth + const customFetch = jest.fn() + + const client = new DrupalClient(BASE_URL, { + auth: customAuth, + fetcher: customFetch, + }) + const url = client.buildUrl("/jsonapi").toString() + + await client.fetch(url, { withAuth: true }) + expect(customFetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + headers: { + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + Authorization: "Basic YXJzaGFkQG5leHQtZHJ1cGFsLm9yZzphYmMxMjM=", + }, + withAuth: true, + }) + ) + }) + + test("accepts clientId and clientSecret", async () => { + const client = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + }) + const fetchSpy = spyOnFetch() + + const basic = Buffer.from( + `${clientIdSecret.clientId}:${clientIdSecret.clientSecret}` + ).toString("base64") + + await client.fetch("http://example.com", { withAuth: true }) + expect(fetchSpy).toHaveBeenNthCalledWith( + 1, + `${BASE_URL}/oauth/token`, + expect.objectContaining({ + headers: { + Accept: "application/json", + Authorization: `Basic ${basic}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + ) + }) + + test("accepts custom auth url", async () => { + const client = new DrupalClient(BASE_URL, { + auth: { + ...clientIdSecret, + url: "/custom/oauth", + }, + }) + const fetchSpy = spyOnFetch() + + await client.fetch("http://example.com", { withAuth: true }) + expect(fetchSpy).toHaveBeenNthCalledWith( + 1, + `${BASE_URL}/custom/oauth`, + expect.anything() + ) + }) + }) + + describe("headers", () => { + // TODO: Are these duplicates of getters-setters/headers tests? + test("allows setting custom headers", async () => { + const customFetch = jest.fn() + const client = new DrupalClient(BASE_URL, { + fetcher: customFetch, + }) + client.headers = { + foo: "bar", + } + + const url = "http://example.com" + + await client.fetch(url) + expect(customFetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + headers: { foo: "bar" }, + }) + ) + }) + + test("allows setting custom headers with custom auth", async () => { + const customFetch = jest.fn() + const client = new DrupalClient(BASE_URL, { + fetcher: customFetch, + headers: { + foo: "bar", + }, + auth: jest + .fn() + .mockReturnValue( + "Basic YXJzaGFkQG5leHQtZHJ1cGFsLm9yZzphYmMxMjM=" + ) as DrupalClientAuth, + }) + + const url = "http://example.com" + + await client.fetch(url, { withAuth: true }) + + expect(customFetch).toHaveBeenCalledWith( + url, + expect.objectContaining({ + headers: { + foo: "bar", + Authorization: "Basic YXJzaGFkQG5leHQtZHJ1cGFsLm9yZzphYmMxMjM=", + }, + withAuth: true, + }) + ) + }) + }) +}) + +describe("getAccessToken()", () => { + const accessToken = mocks.auth.accessToken + const clientIdSecret = mocks.auth.clientIdSecret + + test("uses the long-lived access token from constructor", async () => { + const longLivedAccessToken: AccessToken = { + ...accessToken, + expires_in: 360000, + } + const client = new DrupalClient(BASE_URL, { + accessToken: longLivedAccessToken, + }) + const fetchSpy = spyOnFetch({ + responseBody: { + ...accessToken, + access_token: "not-used", + }, + }) + + const token = await client.getAccessToken({ + clientId: "", + clientSecret: "", + scope: undefined, + }) + expect(fetchSpy).toHaveBeenCalledTimes(0) + expect(token).toBe(longLivedAccessToken) + }) + + test("throws if auth is not configured", async () => { + const fetchSpy = spyOnFetch({ + responseBody: accessToken, + }) + + const client = new DrupalClient(BASE_URL) + + await expect( + // @ts-ignore + client.getAccessToken({ clientId: clientIdSecret.clientId }) + ).rejects.toThrow( + "auth is not configured. See https://next-drupal.org/docs/client/auth" + ) + expect(fetchSpy).toHaveBeenCalledTimes(0) + }) + + test("BUG: throws if auth is ClientIdSecret and not given as opts", async () => { + const fetchSpy = spyOnFetch({ + responseBody: accessToken, + }) + + const client = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + withAuth: true, + }) + + await expect( + // @ts-ignore + client.getAccessToken({ scope: "irrelevant" }) + ).rejects.toThrow( + "'clientId' and 'clientSecret' required. See https://next-drupal.org/docs/client/auth" + ) + expect(fetchSpy).toHaveBeenCalledTimes(0) + }) + + test("fetches an access token", async () => { + spyOnFetch({ + responseBody: accessToken, + }) + + const logger = mockLogger() + const client = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + debug: true, + logger, + }) + + const token = await client.getAccessToken() + expect(token).toEqual(accessToken) + expect(logger.debug).toHaveBeenCalledWith("Fetching new access token.") + }) + + test("re-uses access token", async () => { + spyOnFetchOnce({ + responseBody: accessToken, + }) + const fetchSpy = spyOnFetchOnce({ + responseBody: { + ...accessToken, + access_token: "differentAccessToken", + expires_in: 1800, + }, + }) + + const logger = mockLogger() + const client = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + debug: true, + logger, + }) + + const token1 = await client.getAccessToken() + const token2 = await client.getAccessToken() + expect(token1).toEqual(token2) + expect(logger.debug).toHaveBeenLastCalledWith( + "Using existing access token." + ) + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) +}) + +describe("getEntryForResourceType()", () => { + test("returns the JSON:API entry for a resource type", async () => { + const client = new DrupalClient(BASE_URL) + const getIndexSpy = jest.spyOn(client, "getIndex") + + const recipeEntry = await client.getEntryForResourceType("node--recipe") + expect(recipeEntry).toMatch(`${BASE_URL}/en/jsonapi/node/recipe`) + expect(getIndexSpy).toHaveBeenCalledTimes(1) + + const articleEntry = await client.getEntryForResourceType("node--article") + expect(articleEntry).toMatch(`${BASE_URL}/en/jsonapi/node/article`) + expect(getIndexSpy).toHaveBeenCalledTimes(2) + }) + + test("assembles JSON:API entry without fetching index", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + }) + const getIndexSpy = jest.spyOn(client, "getIndex") + + const recipeEntry = await client.getEntryForResourceType("node--article") + expect(recipeEntry).toMatch(`${BASE_URL}/jsonapi/node/article`) + expect(getIndexSpy).toHaveBeenCalledTimes(0) + }) + + test("throws an error if resource type does not exist", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getEntryForResourceType("RESOURCE-DOES-NOT-EXIST") + ).rejects.toThrow("Resource of type 'RESOURCE-DOES-NOT-EXIST' not found.") + }) +}) + +describe("getIndex()", () => { + test("fetches the JSON:API index", async () => { + const client = new DrupalClient(BASE_URL) + const index = await client.getIndex() + + expect(index).toMatchSnapshot() + }) + + test("fetches the JSON:API index with locale", async () => { + const client = new DrupalClient(BASE_URL) + const index = await client.getIndex("es") + + expect(index).toMatchSnapshot() + }) + + test("throws error for invalid base url", async () => { + const client = new DrupalClient("https://example.com") + + await expect(client.getIndex()).rejects.toThrow( + "Failed to fetch JSON:API index at https://example.com/jsonapi" + ) + }) +}) + +describe("getMenu()", () => { + test("fetches menu items for a menu", async () => { + const client = new DrupalClient(BASE_URL) + + const menu = await client.getMenu("main") + + expect(menu).toMatchSnapshot() + }) + + test("fetches menu items for a menu with locale", async () => { + const client = new DrupalClient(BASE_URL) + + const menu = await client.getMenu("main", { + locale: "es", + defaultLocale: "en", + }) + + expect(menu).toMatchSnapshot() + }) + + test("fetches menu items for a menu with params", async () => { + const client = new DrupalClient(BASE_URL) + + const menu = await client.getMenu("main", { + params: { + "fields[menu_link_content--menu_link_content]": "title", + }, + }) + + expect(menu).toMatchSnapshot() + }) + + test("throws an error for invalid menu name", async () => { + const client = new DrupalClient(BASE_URL) + + await expect(client.getMenu("INVALID")).rejects.toThrow( + '404 Not Found\nThe "menu" parameter was not converted for the path "/jsonapi/menu_items/{menu}" (route name: "jsonapi_menu_items.menu")' + ) + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + + await client.getMenu("main") + expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { + withAuth: false, + }) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + await client.getMenu("main", { withAuth: true }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) +}) + +describe("getResource()", () => { + test("fetches a resource by uuid", async () => { + const client = new DrupalClient(BASE_URL) + const recipe = await client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f" + ) + + expect(recipe).toMatchSnapshot() + }) + + test("fetches a resource by uuid with params", async () => { + const client = new DrupalClient(BASE_URL) + const recipe = await client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + { + params: { + "fields[node--recipe]": "title,field_cooking_time", + }, + } + ) + + expect(recipe).toMatchSnapshot() + }) + + test("fetches a resource using locale", async () => { + const client = new DrupalClient(BASE_URL) + const recipe = await client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + { + locale: "es", + defaultLocale: "en", + params: { + "fields[node--recipe]": "title,field_cooking_time", + }, + } + ) + + expect(recipe).toMatchSnapshot() + }) + + test("fetches raw data", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + { + deserialize: false, + } + ) + ).resolves.toMatchSnapshot() + }) + + test("fetches a resource by revision", async () => { + const client = new DrupalClient(BASE_URL) + const recipe = await client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + { + params: { + "fields[node--recipe]": "drupal_internal__vid", + }, + } + ) + const latestRevision = await client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + { + params: { + resourceVersion: "rel:latest-version", + "fields[node--recipe]": "drupal_internal__vid", + }, + } + ) + + expect(recipe.drupal_internal__vid).toEqual( + latestRevision.drupal_internal__vid + ) + }) + + test("throws an error for invalid revision", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + { + params: { + resourceVersion: "id:-11", + "fields[node--recipe]": "title", + }, + } + ) + ).rejects.toThrow( + "404 Not Found\nThe requested version, identified by `id:-11`, could not be found." + ) + }) + + test("throws an error if revision access is forbidden", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + { + params: { + resourceVersion: "id:1", + "fields[node--recipe]": "title", + }, + } + ) + ).rejects.toThrow( + "403 Forbidden\nThe current user is not allowed to GET the selected resource. The user does not have access to the requested version." + ) + }) + + test("throws an error for invalid resource type", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResource( + "RESOURCE-DOES-NOT-EXIST", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f" + ) + ).rejects.toThrow("Resource of type 'RESOURCE-DOES-NOT-EXIST' not found.") + }) + + test("throws an error for invalid params", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + { + params: { + include: "invalid_relationship", + }, + } + ) + ).rejects.toThrow( + "400 Bad Request\n`invalid_relationship` is not a valid relationship field name. Possible values: node_type, revision_uid, uid, menu_link, field_media_image, field_recipe_category, field_tags." + ) + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + + await client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f" + ) + expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { + withAuth: false, + }) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + await client.getResource( + "node--recipe", + "71e04ead-4cc7-416c-b9ca-60b635fdc50f", + { + withAuth: true, + } + ) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) +}) + +describe("getResourceByPath()", () => { + test("fetches a resource by path", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResourceByPath("/recipes/deep-mediterranean-quiche") + ).resolves.toMatchSnapshot() + }) + + test("fetches a resource by path with params", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResourceByPath("/recipes/deep-mediterranean-quiche", { + params: { + "fields[node--recipe]": "title,field_cooking_time", + }, + }) + ).resolves.toMatchSnapshot() + }) + + test("fetches a resource by path using locale", async () => { + const client = new DrupalClient(BASE_URL) + const recipe = await client.getResourceByPath( + "/recipes/quiche-mediterráneo-profundo", + { + locale: "es", + defaultLocale: "en", + params: { + "fields[node--recipe]": "title,field_cooking_time", + }, + } + ) + + expect(recipe).toMatchSnapshot() + }) + + test("fetches raw data", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResourceByPath("/recipes/deep-mediterranean-quiche", { + deserialize: false, + }) + ).resolves.toMatchSnapshot() + }) + + test("fetches a resource by revision", async () => { + const client = new DrupalClient(BASE_URL) + const recipe = await client.getResourceByPath( + "/recipes/deep-mediterranean-quiche", + { + params: { + "fields[node--recipe]": "drupal_internal__vid", + }, + } + ) + const latestRevision = await client.getResourceByPath( + "/recipes/deep-mediterranean-quiche", + { + params: { + resourceVersion: "rel:latest-version", + "fields[node--recipe]": "drupal_internal__vid", + }, + } + ) + + expect(recipe.drupal_internal__vid).toEqual( + latestRevision.drupal_internal__vid + ) + }) + + test("throws an error for invalid revision", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResourceByPath( + "/recipes/deep-mediterranean-quiche", + { + params: { + resourceVersion: "id:-11", + "fields[node--recipe]": "title", + }, + } + ) + ).rejects.toThrow( + "404 Not Found\nThe requested version, identified by `id:-11`, could not be found." + ) + }) + + test("throws an error if revision access is forbidden", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResourceByPath( + "/recipes/deep-mediterranean-quiche", + { + params: { + resourceVersion: "id:1", + "fields[node--recipe]": "title", + }, + } + ) + ).rejects.toThrow( + "403 Forbidden\nThe current user is not allowed to GET the selected resource. The user does not have access to the requested version." + ) + }) + + test("returns null for path not found", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResourceByPath("/path-do-not-exist") + ).rejects.toThrow("Unable to resolve path /path-do-not-exist.") + }) + + test("throws an error for invalid params", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResourceByPath( + "/recipes/deep-mediterranean-quiche", + { + params: { + include: "invalid_relationship", + }, + } + ) + ).rejects.toThrow( + "400 Bad Request\n`invalid_relationship` is not a valid relationship field name. Possible values: node_type, revision_uid, uid, menu_link, field_media_image, field_recipe_category, field_tags." + ) + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + const getAccessTokenSpy = jest.spyOn(client, "getAccessToken") + + await client.getResourceByPath( + "/recipes/deep-mediterranean-quiche" + ) + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.not.objectContaining({ + withAuth: true, + }) + ) + expect(getAccessTokenSpy).not.toHaveBeenCalled() + }) + + test("makes authenticated requests with withAuth", async () => { + const client = new DrupalClient(BASE_URL, { + auth: mocks.auth.clientIdSecret, + }) + const fetchSpy = spyOnFetch() + const getAccessTokenSpy = jest.spyOn(client, "getAccessToken") + + await client.getResourceByPath( + "/recipes/deep-mediterranean-quiche", + { + withAuth: true, + } + ) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + expect(getAccessTokenSpy).toHaveBeenCalled() + }) + + test("returns null if path is falsey", async () => { + const client = new DrupalClient(BASE_URL) + + const resource = await client.getResourceByPath("") + expect(resource).toBe(null) + }) +}) + +describe("getResourceCollection()", () => { + test("fetches a resource collection", async () => { + const client = new DrupalClient(BASE_URL) + + const articles = await client.getResourceCollection("node--article", { + params: { + "fields[node--article]": "title", + }, + }) + + expect(articles).toMatchSnapshot() + }) + + test("fetches a resource collection using locale", async () => { + const client = new DrupalClient(BASE_URL) + + const articles = await client.getResourceCollection("node--article", { + locale: "es", + defaultLocale: "en", + params: { + "fields[node--article]": "title,langcode", + }, + }) + + expect(articles[0].langcode).toEqual("es") + + expect(articles).toMatchSnapshot() + }) + + test("fetches raw data", async () => { + const client = new DrupalClient(BASE_URL) + + const recipes = await client.getResourceCollection("node--recipe", { + deserialize: false, + params: { + "fields[node--recipe]": "title", + "page[limit]": 2, + }, + }) + + expect(recipes).toMatchSnapshot() + }) + + test("throws an error for invalid resource type", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResourceCollection("RESOURCE-DOES-NOT-EXIST") + ).rejects.toThrow("Resource of type 'RESOURCE-DOES-NOT-EXIST' not found.") + }) + + test("throws an error for invalid params", async () => { + const client = new DrupalClient(BASE_URL) + + await expect( + client.getResourceCollection("node--recipe", { + params: { + include: "invalid_relationship", + }, + }) + ).rejects.toThrow( + "400 Bad Request\n`invalid_relationship` is not a valid relationship field name. Possible values: node_type, revision_uid, uid, menu_link, field_media_image, field_recipe_category, field_tags." + ) + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + + await client.getResourceCollection("node--recipe") + expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { + withAuth: false, + }) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + await client.getResourceCollection("node--recipe", { + withAuth: true, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) +}) + +describe("getSearchIndex()", () => { + test("fetches a search index", async () => { + const client = new DrupalClient(BASE_URL) + + const search = await client.getSearchIndex("recipes", { + params: { + "fields[node--recipe]": "title", + }, + }) + + expect(search).toMatchSnapshot() + }) + + test("fetches a search index with locale", async () => { + const client = new DrupalClient(BASE_URL) + + const search = await client.getSearchIndex("recipes", { + locale: "es", + defaultLocale: "en", + params: { + "fields[node--recipe]": "title", + }, + }) + + expect(search).toMatchSnapshot() + }) + + test("fetches a search index with facets filters", async () => { + const client = new DrupalClient(BASE_URL) + + const search = await client.getSearchIndex( + "recipes", + { + deserialize: false, + params: { + "filter[difficulty]": "easy", + "fields[node--recipe]": "title,field_difficulty", + }, + } + ) + + expect(search).toMatchSnapshot() + expect(search.meta.facets).not.toBeNull() + }) + + test("fetches raw data from search index", async () => { + const client = new DrupalClient(BASE_URL) + + const search = await client.getSearchIndex("recipes", { + deserialize: false, + params: { + "filter[difficulty]": "easy", + "fields[node--recipe]": "title,field_difficulty", + }, + }) + + expect(search).toMatchSnapshot() + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + + await client.getSearchIndex("recipes") + + expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { + withAuth: false, + }) + }) + + test("throws an error for invalid index", async () => { + const client = new DrupalClient(BASE_URL) + + await expect(client.getSearchIndex("INVALID-INDEX")).rejects.toThrow( + "Not Found" + ) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + await client.getSearchIndex("recipes", { + withAuth: true, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) +}) + +describe("getView()", () => { + test("fetches a view", async () => { + const client = new DrupalClient(BASE_URL) + + const view = await client.getView("featured_articles--page_1") + + expect(view).toMatchSnapshot() + }) + + test("fetches a view with params", async () => { + const client = new DrupalClient(BASE_URL) + + const view = await client.getView("featured_articles--page_1", { + params: { + "fields[node--article]": "title", + }, + }) + + expect(view).toMatchSnapshot() + }) + + test("fetches a view with locale", async () => { + const client = new DrupalClient(BASE_URL) + + const view = await client.getView("featured_articles--page_1", { + locale: "es", + defaultLocale: "en", + params: { + "fields[node--article]": "title", + }, + }) + + expect(view).toMatchSnapshot() + }) + + test("fetches raw data", async () => { + const client = new DrupalClient(BASE_URL) + + const view = await client.getView("featured_articles--page_1", { + locale: "es", + defaultLocale: "en", + deserialize: false, + params: { + "fields[node--article]": "title", + }, + }) + + expect(view).toMatchSnapshot() + }) + + test("throws an error for invalid view name", async () => { + const client = new DrupalClient(BASE_URL) + + await expect(client.getView("INVALID")).rejects.toThrow("Not Found") + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + + await client.getView("featured_articles--page_1") + expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { + withAuth: false, + }) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + await client.getView("featured_articles--page_1", { withAuth: true }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) + + test("fetches a view with links for pagination", async () => { + const client = new DrupalClient(BASE_URL) + const view = await client.getView("recipes--page_1") + + expect(view.links).toHaveProperty("next") + }) +}) + +describe("translatePath()", () => { + test("translates a path", async () => { + const client = new DrupalClient(BASE_URL) + + const path = await client.translatePath("recipes/deep-mediterranean-quiche") + + expect(path).toMatchSnapshot() + + const path2 = await client.translatePath( + "/recipes/deep-mediterranean-quiche" + ) + + expect(path).toEqual(path2) + }) + + test("returns null for path not found", async () => { + const client = new DrupalClient(BASE_URL) + + const path = await client.translatePath("/path-not-found") + + expect(path).toBeNull() + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + + await client.translatePath("recipes/deep-mediterranean-quiche") + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: false, + }) + ) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + await client.translatePath("recipes/deep-mediterranean-quiche", { + withAuth: true, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) +}) diff --git a/packages/next-drupal/tests/DrupalClient/getters-setters.test.ts b/packages/next-drupal/tests/DrupalClient/getters-setters.test.ts new file mode 100644 index 00000000..ce224196 --- /dev/null +++ b/packages/next-drupal/tests/DrupalClient/getters-setters.test.ts @@ -0,0 +1,224 @@ +import { afterEach, describe, expect, jest, test } from "@jest/globals" +import { + AccessToken, + DrupalClient, + DrupalClientAuthAccessToken, + DrupalClientAuthUsernamePassword, + DrupalClientOptions, +} from "../../src" +import { BASE_URL, mocks } from "../utils" + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe("apiPrefix", () => { + test("get apiPrefix", () => { + const client = new DrupalClient(BASE_URL) + expect(client.apiPrefix).toBe("/jsonapi") + }) + test("set apiPrefix", () => { + const client = new DrupalClient(BASE_URL) + client.apiPrefix = "/api" + expect(client.apiPrefix).toBe("/api") + }) + test('set apiPrefix and prefixes with "/"', () => { + const client = new DrupalClient(BASE_URL) + client.apiPrefix = "api" + expect(client.apiPrefix).toBe("/api") + }) +}) + +describe("auth", () => { + describe("throws an error if invalid Basic Auth", () => { + test("missing username", () => { + expect(() => { + const client = new DrupalClient(BASE_URL) + // @ts-ignore + client.auth = { + password: "password", + } + }).toThrow( + "'username' and 'password' are required for auth. See https://next-drupal.org/docs/client/auth" + ) + }) + + test("missing password", () => { + expect(() => { + const client = new DrupalClient(BASE_URL) + // @ts-ignore + client.auth = { + username: "admin", + } + }).toThrow( + "'username' and 'password' are required for auth. See https://next-drupal.org/docs/client/auth" + ) + }) + }) + + describe("throws an error if invalid Access Token", () => { + // TODO: The wrong error is thrown. + test.skip("missing access_token", () => { + expect(() => { + const client = new DrupalClient(BASE_URL) + // @ts-ignore + client.auth = { + token_type: "bearer", + } + }).toThrow( + "'access_token' and 'token_type' are required for auth. See https://next-drupal.org/docs/client/auth" + ) + }) + + test("missing token_type", () => { + expect(() => { + const client = new DrupalClient(BASE_URL) + // @ts-ignore + client.auth = { + access_token: mocks.auth.accessToken.access_token, + } + }).toThrow( + "'access_token' and 'token_type' are required for auth. See https://next-drupal.org/docs/client/auth" + ) + }) + }) + + describe("throws an error if invalid Client ID/Secret", () => { + test("missing clientId", () => { + expect(() => { + const client = new DrupalClient(BASE_URL) + // @ts-ignore + client.auth = { + clientSecret: mocks.auth.clientIdSecret.clientSecret, + } + }).toThrow( + "'clientId' and 'clientSecret' are required for auth. See https://next-drupal.org/docs/client/auth" + ) + }) + + test("missing clientSecret", () => { + expect(() => { + const client = new DrupalClient(BASE_URL) + // @ts-ignore + client.auth = { + clientId: mocks.auth.clientIdSecret.clientId, + } + }).toThrow( + "'clientId' and 'clientSecret' are required for auth. See https://next-drupal.org/docs/client/auth" + ) + }) + }) + + test("sets Basic Auth", () => { + const basicAuth: DrupalClientAuthUsernamePassword = { + ...mocks.auth.basicAuth, + } + const client = new DrupalClient(BASE_URL) + client.auth = basicAuth + expect(client._auth).toMatchObject({ ...basicAuth }) + }) + + test("sets Access Token", () => { + const accessToken = { + ...mocks.auth.accessToken, + } + const client = new DrupalClient(BASE_URL) + client.auth = accessToken + expect(client._auth).toMatchObject({ ...accessToken }) + }) + + test("sets Client ID/Secret", () => { + const clientIdSecret = { + ...mocks.auth.clientIdSecret, + } + const client = new DrupalClient(BASE_URL) + client.auth = clientIdSecret + expect(client._auth).toMatchObject({ ...clientIdSecret }) + }) + + test("sets auth function", () => { + const authFunction = mocks.auth.function + const client = new DrupalClient(BASE_URL) + client.auth = authFunction + expect(client._auth).toBe(authFunction) + }) + + test("sets custom Authorization string", () => { + const authString = `${mocks.auth.customAuthenticationHeader}` + const client = new DrupalClient(BASE_URL) + client.auth = authString + expect(client._auth).toBe(authString) + }) + + test("sets a default access token url", () => { + const accessToken = { + ...mocks.auth.accessToken, + } + const client = new DrupalClient(BASE_URL) + client.auth = accessToken + expect(client._auth.url).toBe("/oauth/token") + }) + + test("can override the default access token url", () => { + const accessToken = { + ...mocks.auth.accessToken, + url: "/custom/oauth/token", + } + const client = new DrupalClient(BASE_URL) + client.auth = accessToken + expect(client._auth.url).toBe("/custom/oauth/token") + }) +}) + +describe("headers", () => { + describe("set headers", () => { + test("using key-value pairs", () => { + const headers = [ + ["Content-Type", "application/x-www-form-urlencoded"], + ["Accept", "application/json"], + ] as DrupalClientOptions["headers"] + const client = new DrupalClient(BASE_URL) + client.headers = headers + expect(client._headers).toBe(headers) + }) + + test("using object literal", () => { + const headers = { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + } as DrupalClientOptions["headers"] + const client = new DrupalClient(BASE_URL) + client.headers = headers + expect(client._headers).toBe(headers) + }) + + test("using Headers object", () => { + const headers = new Headers() + headers.append("Content-Type", "application/x-www-form-urlencoded") + headers.append("Accept", "application/json") + + const client = new DrupalClient(BASE_URL) + client.headers = headers + expect(client._headers).toBe(headers) + }) + }) +}) + +describe("token", () => { + test("set token", () => { + function getExpiresOn(token: AccessToken): number { + return Date.now() + token.expires_in * 1000 + } + + const accessToken = { + ...mocks.auth.accessToken, + } as DrupalClientAuthAccessToken + const before = getExpiresOn(accessToken) + + const client = new DrupalClient(BASE_URL) + client.token = accessToken + expect(client._token).toBe(accessToken) + expect(client.tokenExpiresOn).toBeGreaterThanOrEqual(before) + expect(client.tokenExpiresOn).toBeLessThanOrEqual(getExpiresOn(accessToken)) + }) +}) diff --git a/packages/next-drupal/tests/DrupalClient/pages-router-methods.test.ts b/packages/next-drupal/tests/DrupalClient/pages-router-methods.test.ts new file mode 100644 index 00000000..fa4679b5 --- /dev/null +++ b/packages/next-drupal/tests/DrupalClient/pages-router-methods.test.ts @@ -0,0 +1,1252 @@ +import { afterEach, describe, expect, jest, test } from "@jest/globals" +import { GetStaticPropsContext } from "next" +import { DrupalClient } from "../../src" +import { BASE_URL, mocks, spyOnFetch } from "../utils" +import type { DrupalNode, JsonApiResourceWithPath } from "../../src" + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe("buildStaticPathsFromResources()", () => { + const resources: Pick[] = [ + { + path: { + alias: "/blog/post/one", + pid: 1, + langcode: "en", + }, + }, + { + path: { + alias: "/blog/post/two", + pid: 2, + langcode: "en", + }, + }, + ] + + test("builds static paths from resources", () => { + const client = new DrupalClient(BASE_URL) + + expect(client.buildStaticPathsFromResources(resources)).toMatchObject([ + { + params: { + slug: ["blog", "post", "one"], + }, + }, + { + params: { + slug: ["blog", "post", "two"], + }, + }, + ]) + + expect( + client.buildStaticPathsFromResources(resources, { locale: "es" }) + ).toMatchObject([ + { + locale: "es", + params: { + slug: ["blog", "post", "one"], + }, + }, + { + locale: "es", + params: { + slug: ["blog", "post", "two"], + }, + }, + ]) + }) + + test("builds static paths from resources with pathPrefix", () => { + const client = new DrupalClient(BASE_URL) + + const paths = client.buildStaticPathsFromResources(resources, { + pathPrefix: "blog", + }) + + const paths2 = client.buildStaticPathsFromResources(resources, { + pathPrefix: "/blog", + }) + + const paths3 = client.buildStaticPathsFromResources(resources, { + pathPrefix: "/blog/post", + locale: "es", + }) + + const paths4 = client.buildStaticPathsFromResources(resources, { + pathPrefix: "blog/post", + locale: "es", + }) + + expect(paths).toMatchObject([ + { + params: { + slug: ["post", "one"], + }, + }, + { + params: { + slug: ["post", "two"], + }, + }, + ]) + expect(paths3).toMatchObject([ + { + locale: "es", + params: { + slug: ["one"], + }, + }, + { + locale: "es", + params: { + slug: ["two"], + }, + }, + ]) + + expect(paths).toEqual(paths2) + expect(paths3).toEqual(paths4) + }) + + test('converts frontPage path to "/"', () => { + const client = new DrupalClient(BASE_URL) + + const resources: Pick[] = [ + { + path: { + alias: "/home", + pid: 1, + langcode: "en", + }, + }, + ] + + expect(client.buildStaticPathsFromResources(resources)).toMatchObject([ + { + params: { + slug: [""], + }, + }, + ]) + }) +}) + +describe("buildStaticPathsParamsFromPaths()", () => { + test("builds static paths from paths", () => { + const client = new DrupalClient(BASE_URL) + + const paths = ["/blog/post/one", "/blog/post/two", "/blog/post/three"] + + expect(client.buildStaticPathsParamsFromPaths(paths)).toMatchObject([ + { + params: { + slug: ["blog", "post", "one"], + }, + }, + { + params: { + slug: ["blog", "post", "two"], + }, + }, + { + params: { + slug: ["blog", "post", "three"], + }, + }, + ]) + + expect( + client.buildStaticPathsParamsFromPaths(paths, { locale: "en" }) + ).toMatchObject([ + { + locale: "en", + params: { + slug: ["blog", "post", "one"], + }, + }, + { + locale: "en", + params: { + slug: ["blog", "post", "two"], + }, + }, + { + locale: "en", + params: { + slug: ["blog", "post", "three"], + }, + }, + ]) + }) + + test("builds static paths from paths with pathPrefix", () => { + const client = new DrupalClient(BASE_URL) + + const paths = client.buildStaticPathsParamsFromPaths( + ["/blog/post/one", "/blog/post/two", "/blog/post"], + { pathPrefix: "blog" } + ) + + const paths2 = client.buildStaticPathsParamsFromPaths( + ["/blog/post/one", "/blog/post/two", "/blog/post"], + { pathPrefix: "/blog" } + ) + + const paths3 = client.buildStaticPathsParamsFromPaths( + ["blog/post/one", "blog/post/two", "blog/post"], + { pathPrefix: "/blog" } + ) + + const paths4 = client.buildStaticPathsParamsFromPaths( + ["blog/post/one", "blog/post/two", "blog/post"], + { pathPrefix: "blog" } + ) + + expect(paths).toMatchObject([ + { + params: { + slug: ["post", "one"], + }, + }, + { + params: { + slug: ["post", "two"], + }, + }, + { + params: { + slug: ["post"], + }, + }, + ]) + + expect(paths).toEqual(paths2) + expect(paths).toEqual(paths3) + expect(paths).toEqual(paths4) + }) +}) + +describe("getAuthFromContextAndOptions()", () => { + const clientIdSecret = mocks.auth.clientIdSecret + + test("should use the withAuth option if provided and NOT in preview", async () => { + const client = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + await client.getResourceFromContext( + "node--article", + { + preview: false, + }, + { + withAuth: true, + } + ) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + + await client.getResourceFromContext( + "node--article", + { + preview: false, + }, + { + withAuth: { + clientId: "foo", + clientSecret: "bar", + scope: "baz", + }, + } + ) + + expect(fetchSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: { + clientId: "foo", + clientSecret: "bar", + scope: "baz", + }, + }) + ) + }) + + test("should fallback to the global auth if NOT in preview and no withAuth option provided", async () => { + const client = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + }) + const fetchSpy = spyOnFetch() + + await client.getResourceFromContext("node--article", { + preview: false, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: false, + }) + ) + + const client2 = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + withAuth: true, + }) + jest.spyOn(client2, "getAccessToken").mockImplementation(async () => ({ + token_type: "", + expires_in: 0, + access_token: "", + })) + + await client2.getResourceFromContext("node--article", { + preview: false, + }) + + expect(fetchSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) + + test("should NOT use the global auth if in preview", async () => { + const client = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + withAuth: true, + }) + const fetchSpy = spyOnFetch() + + await client.getResourceFromContext("node--article", { + preview: true, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: null, + }) + ) + }) + + test("should use the scope from context if in preview and using the simple_oauth plugin", async () => { + const client = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + }) + const fetchSpy = spyOnFetch() + + await client.getResourceFromContext("node--article", { + preview: true, + previewData: { + plugin: "simple_oauth", + scope: "editor", + }, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: { + ...clientIdSecret, + scope: "editor", + url: "/oauth/token", + }, + }) + ) + }) + + test("should use the scope from context even with global withAuth if in preview and using the simple_oauth plugin", async () => { + const client = new DrupalClient(BASE_URL, { + auth: { + ...clientIdSecret, + scope: "administrator", + }, + withAuth: true, + }) + const fetchSpy = spyOnFetch() + + await client.getResourceFromContext("node--article", { + preview: true, + previewData: { + plugin: "simple_oauth", + scope: "editor", + }, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: { + ...clientIdSecret, + scope: "editor", + url: "/oauth/token", + }, + }) + ) + }) + + test("should use the access_token from context if in preview and using the jwt plugin", async () => { + const client = new DrupalClient(BASE_URL, { + auth: clientIdSecret, + }) + const fetchSpy = spyOnFetch() + + await client.getResourceFromContext("node--article", { + preview: true, + previewData: { + plugin: "jwt", + access_token: "example-token", + }, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: `Bearer example-token`, + }) + ) + }) + + test("should use the access token from context even with global withAuth if in preview and using the jwt plugin", async () => { + const client = new DrupalClient(BASE_URL, { + auth: { + ...clientIdSecret, + scope: "administrator", + }, + withAuth: true, + }) + const fetchSpy = spyOnFetch() + + await client.getResourceFromContext("node--article", { + preview: true, + previewData: { + plugin: "jwt", + access_token: "example-token", + }, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: `Bearer example-token`, + }) + ) + }) +}) + +describe("getPathFromContext()", () => { + test("returns a path from context", async () => { + const client = new DrupalClient(BASE_URL) + + expect( + client.getPathFromContext({ + params: { + slug: ["foo"], + }, + }) + ).toEqual("/foo") + + expect( + client.getPathFromContext({ + params: { + slug: ["foo", "bar"], + }, + }) + ).toEqual("/foo/bar") + + expect( + client.getPathFromContext({ + locale: "en", + defaultLocale: "es", + params: { + slug: ["foo", "bar"], + }, + }) + ).toEqual("/en/foo/bar") + + expect( + client.getPathFromContext({ + params: { + slug: [], + }, + }) + ).toEqual("/home") + + client.frontPage = "/front" + + expect( + client.getPathFromContext({ + params: { + slug: [], + }, + }) + ).toEqual("/front") + + expect( + client.getPathFromContext({ + locale: "es", + defaultLocale: "en", + params: { + slug: [], + }, + }) + ).toEqual("/es/front") + }) + + test("returns a path from context with pathPrefix", () => { + const client = new DrupalClient(BASE_URL) + + expect( + client.getPathFromContext( + { + params: { + slug: ["bar", "baz"], + }, + }, + { + pathPrefix: "/foo", + } + ) + ).toEqual("/foo/bar/baz") + + expect( + client.getPathFromContext( + { + params: { + slug: ["bar", "baz"], + }, + }, + { + pathPrefix: "foo", + } + ) + ).toEqual("/foo/bar/baz") + + expect( + client.getPathFromContext( + { + locale: "en", + defaultLocale: "en", + params: { + slug: ["bar", "baz"], + }, + }, + { + pathPrefix: "foo", + } + ) + ).toEqual("/foo/bar/baz") + + expect( + client.getPathFromContext( + { + locale: "es", + defaultLocale: "en", + params: { + slug: ["bar", "baz"], + }, + }, + { + pathPrefix: "foo", + } + ) + ).toEqual("/es/foo/bar/baz") + + expect( + client.getPathFromContext( + { + locale: "es", + defaultLocale: "en", + params: { + slug: [], + }, + }, + { + pathPrefix: "/foo", + } + ) + ).toEqual("/es/foo/home") + + client.frontPage = "/baz" + + expect( + client.getPathFromContext( + { + locale: "en", + defaultLocale: "en", + params: { + slug: [], + }, + }, + { + pathPrefix: "foo", + } + ) + ).toEqual("/foo/baz") + + expect( + client.getPathFromContext( + { + params: { + slug: [], + }, + }, + { + pathPrefix: "/foo/bar", + } + ) + ).toEqual("/foo/bar/baz") + }) + + test("encodes path with punctuation", async () => { + const client = new DrupalClient(BASE_URL) + + const path = client.getPathFromContext({ + params: { + slug: ["path&with^punc&in$path"], + }, + }) + + expect(path).toEqual("/path%26with%5Epunc%26in%24path") + + const translatedPath = await client.translatePath(path) + + expect(translatedPath).toMatchSnapshot() + }) +}) + +describe("getPathsFromContext()", () => { + test("is an alias for getStaticPathsFromContext", () => { + const client = new DrupalClient(BASE_URL) + expect(client.getPathsFromContext).toBe(client.getStaticPathsFromContext) + }) +}) + +describe("getResourceCollectionFromContext()", () => { + test("fetches a resource collection", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + locale: "en", + defaultLocale: "en", + } + + const articles = await client.getResourceCollectionFromContext( + "node--article", + context, + { + params: { + "fields[node--article]": "title", + }, + } + ) + + expect(articles).toMatchSnapshot() + }) + + test("fetches a resource collection using locale", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + locale: "es", + defaultLocale: "en", + } + + const articles = await client.getResourceCollectionFromContext( + "node--article", + context, + { + params: { + "fields[node--article]": "title,langcode", + }, + } + ) + + expect(articles[0].langcode).toEqual("es") + + expect(articles).toMatchSnapshot() + }) + + test("fetches raw data", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + locale: "en", + defaultLocale: "en", + } + + const recipes = await client.getResourceCollectionFromContext( + "node--recipe", + context, + { + deserialize: false, + params: { + "fields[node--recipe]": "title", + "page[limit]": 2, + }, + } + ) + + expect(recipes).toMatchSnapshot() + }) + + test("throws an error for invalid resource type", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + locale: "en", + defaultLocale: "en", + } + + await expect( + client.getResourceCollectionFromContext( + "RESOURCE-DOES-NOT-EXIST", + context + ) + ).rejects.toThrow("Resource of type 'RESOURCE-DOES-NOT-EXIST' not found.") + }) + + test("throws an error for invalid params", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + locale: "en", + defaultLocale: "en", + } + + await expect( + client.getResourceCollectionFromContext( + "node--recipe", + context, + { + params: { + include: "invalid_relationship", + }, + } + ) + ).rejects.toThrow( + "400 Bad Request\n`invalid_relationship` is not a valid relationship field name. Possible values: node_type, revision_uid, uid, menu_link, field_media_image, field_recipe_category, field_tags." + ) + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + + const context: GetStaticPropsContext = { + locale: "en", + defaultLocale: "en", + } + + await client.getResourceCollectionFromContext("node--recipe", context) + expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { + withAuth: false, + }) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + const context: GetStaticPropsContext = { + locale: "en", + defaultLocale: "en", + } + await client.getResourceCollectionFromContext("node--recipe", context, { + withAuth: true, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) +}) + +describe("getResourceFromContext()", () => { + test("fetches a resource from context", async () => { + const client = new DrupalClient(BASE_URL) + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + const recipe = await client.getResourceFromContext( + "node--recipe", + context + ) + + expect(recipe).toMatchSnapshot() + }) + + test("fetches a resource from context with params", async () => { + const client = new DrupalClient(BASE_URL) + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + const recipe = await client.getResourceFromContext( + "node--recipe", + context, + { + params: { + "fields[node--recipe]": "title", + }, + } + ) + + expect(recipe).toMatchSnapshot() + }) + + test("fetches a resource from context using locale", async () => { + const client = new DrupalClient(BASE_URL) + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "quiche-mediterráneo-profundo"], + }, + locale: "es", + defaultLocale: "en", + } + const recipe = await client.getResourceFromContext( + "node--recipe", + context, + { + params: { + "fields[node--recipe]": "title,field_cooking_time", + }, + } + ) + + expect(recipe).toMatchSnapshot() + }) + + test("fetches raw data", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + const recipe = await client.getResourceFromContext( + "node--recipe", + context, + { + deserialize: false, + params: { + "fields[node--recipe]": "title", + }, + } + ) + + expect(recipe).toMatchSnapshot() + }) + + test("fetches a resource from context by revision", async () => { + const client = new DrupalClient(BASE_URL) + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "quiche-mediterráneo-profundo"], + }, + locale: "es", + defaultLocale: "en", + } + const recipe = await client.getResourceFromContext( + "node--recipe", + context, + { + params: { + "fields[node--recipe]": "drupal_internal__vid", + }, + } + ) + + context.previewData = { resourceVersion: "rel:latest-version" } + + const latestRevision = await client.getResourceFromContext( + "node--recipe", + context, + { + params: { + "fields[node--recipe]": "drupal_internal__vid", + }, + } + ) + + expect(recipe.drupal_internal__vid).toEqual( + latestRevision.drupal_internal__vid + ) + }) + + test("throws an error for invalid revision", async () => { + const client = new DrupalClient(BASE_URL) + const context: GetStaticPropsContext = { + previewData: { + resourceVersion: "id:-11", + }, + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + + await expect( + client.getResourceFromContext("node--recipe", context, { + params: { + "fields[node--recipe]": "drupal_internal__vid", + }, + }) + ).rejects.toThrow( + "404 Not Found\nThe requested version, identified by `id:-11`, could not be found." + ) + }) + + test("throws an error if revision access is forbidden", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + previewData: { + resourceVersion: "id:1", + }, + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + + await expect( + client.getResourceFromContext("node--recipe", context, { + params: { + "fields[node--recipe]": "title", + }, + }) + ).rejects.toThrow( + "403 Forbidden\nThe current user is not allowed to GET the selected resource. The user does not have access to the requested version." + ) + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + + await client.getResourceFromContext("node--recipe", context) + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: false, + }) + ) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + + await client.getResourceFromContext("node--recipe", context, { + withAuth: true, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) + + test("makes authenticated requests when preview is true", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + const context: GetStaticPropsContext = { + preview: true, + previewData: { + plugin: "simple_oauth", + scope: "editor", + }, + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + + await client.getResourceFromContext("node--recipe", context) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: `Bearer sample-token`, + }) + ) + }) + + test("accepts a translated path", async () => { + const client = new DrupalClient(BASE_URL) + + const path = await client.translatePath("recipes/deep-mediterranean-quiche") + + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + + const recipe = await client.getResourceFromContext(path, context, { + params: { + "fields[node--recipe]": "title,path,status", + }, + }) + + await expect(recipe).toMatchSnapshot() + }) +}) + +describe("getSearchIndexFromContext()", () => { + test("calls getSearchIndex() with context data", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest + .spyOn(client, "getSearchIndex") + .mockImplementation(async () => jest.fn()) + const name = "resource-name" + const locale = "en-uk" + const defaultLocale = "en-us" + const options = { + deserialize: true, + } + + await client.getSearchIndexFromContext( + name, + { locale, defaultLocale }, + options + ) + + expect(fetchSpy).toHaveBeenCalledWith(name, { + ...options, + locale, + defaultLocale, + }) + }) +}) + +describe("getStaticPathsFromContext()", () => { + test("returns static paths from context", async () => { + const client = new DrupalClient(BASE_URL) + + const paths = await client.getStaticPathsFromContext("node--article", {}) + + expect(paths).toMatchSnapshot() + }) + + test("returns static paths from context with locale", async () => { + const client = new DrupalClient(BASE_URL) + + const paths = await client.getStaticPathsFromContext("node--article", { + locales: ["en", "es"], + defaultLocale: "en", + }) + + expect(paths).toMatchSnapshot() + }) + + test("returns static paths for multiple resource types from context", async () => { + const client = new DrupalClient(BASE_URL) + + const paths = await client.getStaticPathsFromContext( + ["node--article", "node--recipe"], + { + locales: ["en", "es"], + defaultLocale: "en", + } + ) + + expect(paths).toMatchSnapshot() + }) + + test("returns static paths from context with params", async () => { + const client = new DrupalClient(BASE_URL) + + const paths = await client.getStaticPathsFromContext( + "node--article", + {}, + { + params: { + "filter[promote]": 1, + }, + } + ) + + expect(paths).toMatchSnapshot() + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + + await client.getStaticPathsFromContext("node--article", { + locales: ["en", "es"], + defaultLocale: "en", + }) + expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { + withAuth: false, + }) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + await client.getStaticPathsFromContext( + "node--article", + { + locales: ["en", "es"], + defaultLocale: "en", + }, + { + withAuth: true, + } + ) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) +}) + +describe("translatePathFromContext()", () => { + test("translates a path", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + + const path = await client.translatePathFromContext(context) + + expect(path).toMatchSnapshot() + }) + + test("returns null for path not found", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + params: { + slug: ["path-not-found"], + }, + } + + const path = await client.translatePathFromContext(context) + + expect(path).toBeNull() + }) + + test("translates a path with pathPrefix", async () => { + const client = new DrupalClient(BASE_URL) + + const context: GetStaticPropsContext = { + params: { + slug: ["deep-mediterranean-quiche"], + }, + } + + const path = await client.translatePathFromContext(context, { + pathPrefix: "recipes", + }) + + expect(path).toMatchSnapshot() + + const path2 = await client.translatePathFromContext(context, { + pathPrefix: "/recipes", + }) + + expect(path).toEqual(path2) + }) + + test("makes un-authenticated requests by default", async () => { + const client = new DrupalClient(BASE_URL) + const fetchSpy = jest.spyOn(client, "fetch") + + const context: GetStaticPropsContext = { + params: { + slug: ["recipes", "deep-mediterranean-quiche"], + }, + } + await client.translatePathFromContext(context) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: false, + }) + ) + }) + + test("makes authenticated requests with withAuth option", async () => { + const client = new DrupalClient(BASE_URL, { + useDefaultResourceTypeEntry: true, + auth: `Bearer sample-token`, + }) + const fetchSpy = spyOnFetch() + jest.spyOn(client, "getAccessToken") + + const context: GetStaticPropsContext = { + params: { + slug: ["deep-mediterranean-quiche"], + }, + } + await client.translatePathFromContext(context, { + pathPrefix: "recipes", + withAuth: true, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withAuth: true, + }) + ) + }) +}) diff --git a/packages/next-drupal/tests/Logger/logger.test.ts b/packages/next-drupal/tests/Logger/logger.test.ts new file mode 100644 index 00000000..4ecf4150 --- /dev/null +++ b/packages/next-drupal/tests/Logger/logger.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, jest, test } from "@jest/globals" +import { + LOG_MESSAGE_PREFIX, + DEBUG_MESSAGE_PREFIX, + WARN_MESSAGE_PREFIX, + ERROR_MESSAGE_PREFIX, + logger, +} from "../../src/logger" +import type { Logger } from "../../src" + +test("is type Logger", () => { + // At compile time, compilation will fail if not a Logger type. + const test: Logger = logger + + // At run-time, we just check for object. + expect(typeof test === "object").toBe(true) +}) + +describe("method: debug", () => { + test("logs a message", () => { + const consoleSpy = jest + .spyOn(console, "debug") + .mockImplementation((message) => message) + const message = "Test debug message" + + logger.debug(message) + + expect(consoleSpy).toHaveBeenCalledWith(DEBUG_MESSAGE_PREFIX, message) + }) +}) + +describe("method: error", () => { + test("logs a message", () => { + const consoleSpy = jest + .spyOn(console, "error") + .mockImplementation((message) => message) + const message = "Test error message" + + logger.error(message) + + expect(consoleSpy).toHaveBeenCalledWith(ERROR_MESSAGE_PREFIX, message) + }) +}) + +describe("method: log", () => { + test("logs a message", () => { + const consoleSpy = jest + .spyOn(console, "log") + .mockImplementation((message) => message) + const message = "Test log message" + + logger.log(message) + + expect(consoleSpy).toHaveBeenCalledWith(LOG_MESSAGE_PREFIX, message) + }) +}) + +describe("method: warn", () => { + test("logs a message", () => { + const consoleSpy = jest + .spyOn(console, "warn") + .mockImplementation((message) => message) + const message = "Test warn message" + + logger.warn(message) + + expect(consoleSpy).toHaveBeenCalledWith(WARN_MESSAGE_PREFIX, message) + }) +}) diff --git a/packages/next-drupal/tests/__mocks__/next.ts b/packages/next-drupal/tests/__mocks__/next.ts new file mode 100644 index 00000000..664b58ba --- /dev/null +++ b/packages/next-drupal/tests/__mocks__/next.ts @@ -0,0 +1,37 @@ +export const NextApiRequest = jest.fn(function () { + this.query = { + slug: "/example", + resourceVersion: "id:1", + plugin: "simple_oauth", + secret: "very-secret-key", + } + this.url = `https://example.com/?${new URLSearchParams(this.query)}` + this.headers = { + host: "https://example.com", + } +}) + +export const NextApiResponse = jest.fn(function () { + const headers = { + "Set-Cookie": ["mock-cookie-value"], + } + const response = { + statusCode: 200, + status: jest.fn((statusCode) => { + response.statusCode = statusCode + return response + }), + clearPreviewData: jest.fn(() => response), + setPreviewData: jest.fn(() => response), + setDraftMode: jest.fn(() => response), + getHeader: jest.fn((name) => headers[name]), + setHeader: jest.fn((name, value) => { + headers[name] = value + return response + }), + writeHead: jest.fn(() => response), + end: jest.fn(), + json: jest.fn(), + } + return response +}) diff --git a/packages/next-drupal/tests/__mocks__/next/headers.ts b/packages/next-drupal/tests/__mocks__/next/headers.ts new file mode 100644 index 00000000..ca2da914 --- /dev/null +++ b/packages/next-drupal/tests/__mocks__/next/headers.ts @@ -0,0 +1,86 @@ +import type { CookieListItem } from "next/dist/compiled/@edge-runtime/cookies" + +// Create nested mocks ahead of time instead of always creating new mocks when +// the cookies mock function is called. +export const cookies = jest.fn(() => cookiesObject) + +const cookieStore = {} +const cookiesObject = { + delete: jest.fn((name: string) => { + delete cookieStore[name] + return cookiesObject + }), + get: jest.fn((name: string) => + cookieStore[name] + ? ({ + ...cookieStore[name], + } as CookieListItem) + : undefined + ), + getAll: jest.fn( + () => + [ + ...Object.keys(cookieStore).map((name) => ({ + ...cookieStore[name], + })), + ] as CookieListItem[] + ), + has: jest.fn((name: string) => !!cookieStore[name]), + set: jest.fn( + ({ + name, + value, + expires, + sameSite, + secure, + path, + domain, + }: CookieListItem) => { + cookieStore[name] = { + name, + value, + expires, + sameSite, + secure, + path, + domain, + } + return cookiesObject + } + ), + toString: jest.fn(), +} + +// Create nested mocks ahead of time instead of always creating new mocks when +// the draftMode mock function is called. +export const draftMode = jest.fn(() => ({ + disable, + enable, + isEnabled: draftModeEnabled, +})) + +let draftModeEnabled = false +const disable = jest.fn(() => { + draftModeEnabled = false +}) +const enable = jest.fn(() => { + draftModeEnabled = true +}) + +export function resetNextHeaders() { + cookies.mockClear() + cookiesObject.delete.mockClear() + cookiesObject.get.mockClear() + cookiesObject.getAll.mockClear() + cookiesObject.has.mockClear() + cookiesObject.set.mockClear() + cookiesObject.toString.mockClear() + Object.keys(cookieStore).forEach((key) => { + delete cookieStore[key] + }) + + draftMode.mockClear() + disable.mockClear() + enable.mockClear() + draftModeEnabled = false +} diff --git a/packages/next-drupal/tests/client.test.ts b/packages/next-drupal/tests/client.test.ts deleted file mode 100644 index aeacea93..00000000 --- a/packages/next-drupal/tests/client.test.ts +++ /dev/null @@ -1,2647 +0,0 @@ -import { expect } from "@jest/globals" -import { GetStaticPropsContext } from "next" -import { DrupalClient } from "../src/client" -import type { - Serializer, - DrupalNode, - Logger, - JsonApiResourceWithPath, - JsonApiSearchApiResponse, -} from "../src/types" -import { BASE_URL } from "./utils" - -jest.setTimeout(10000) - -afterEach(() => { - jest.restoreAllMocks() -}) - -describe("DrupalClient", () => { - test("it properly constructs a DrupalClient", () => { - expect(new DrupalClient(BASE_URL)).toBeInstanceOf(DrupalClient) - }) - - test("it throws error for invalid baseUrl", () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(() => new DrupalClient()).toThrow("The 'baseUrl' param is required.") - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(() => new DrupalClient({})).toThrow( - "The 'baseUrl' param is required." - ) - }) - - test("it correctly formats apiPrefix", () => { - const client = new DrupalClient(BASE_URL) - expect(client.apiPrefix).toBe("/jsonapi") - - const client2 = new DrupalClient(BASE_URL, { - apiPrefix: "/api", - }) - expect(client2.apiPrefix).toBe("/api") - - const client3 = new DrupalClient(BASE_URL, {}) - client3.apiPrefix = "api" - expect(client3.apiPrefix).toBe("/api") - - const client4 = new DrupalClient(BASE_URL, {}) - expect(client4.apiPrefix).toBe("/jsonapi") - }) - - test("it has a debug mode", async () => { - const consoleSpy = jest.spyOn(console, "debug").mockImplementation() - - const client = new DrupalClient(BASE_URL, { - debug: true, - }) - - expect(consoleSpy).toHaveBeenCalledWith( - "[next-drupal][debug]:", - "Debug mode is on." - ) - expect(client.isDebugEnabled).toBe(true) - }) -}) - -describe("auth", () => { - test("it accepts username and password for auth", async () => { - const customFetch = jest.fn() - - const client = new DrupalClient(BASE_URL, { - auth: { - username: "admin", - password: "password", - }, - fetcher: customFetch, - }) - const url = client.buildUrl("/jsonapi").toString() - - await client.fetch(url, { withAuth: true }) - expect(customFetch).toHaveBeenCalledWith( - url, - expect.objectContaining({ - headers: { - "Content-Type": "application/vnd.api+json", - Accept: "application/vnd.api+json", - Authorization: "Basic YWRtaW46cGFzc3dvcmQ=", - }, - withAuth: true, - }) - ) - }) - - test("it accepts callback for auth", async () => { - const customAuth = jest - .fn() - .mockReturnValue("Basic YXJzaGFkQG5leHQtZHJ1cGFsLm9yZzphYmMxMjM=") - const customFetch = jest.fn() - - const client = new DrupalClient(BASE_URL, { - auth: customAuth, - fetcher: customFetch, - }) - const url = client.buildUrl("/jsonapi").toString() - - await client.fetch(url, { withAuth: true }) - expect(customFetch).toHaveBeenCalledWith( - url, - expect.objectContaining({ - headers: { - "Content-Type": "application/vnd.api+json", - Accept: "application/vnd.api+json", - Authorization: "Basic YXJzaGFkQG5leHQtZHJ1cGFsLm9yZzphYmMxMjM=", - }, - withAuth: true, - }) - ) - }) - - test("it accepts clientId and clientSecret for auth", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - - const basic = Buffer.from( - `7795065e-8ad0-45eb-a64d-73d9f3a5e943:d92Fm^ds` - ).toString("base64") - - await client.fetch("http://example.com", { withAuth: true }) - expect(fetchSpy).toHaveBeenNthCalledWith( - 1, - `${BASE_URL}/oauth/token`, - expect.objectContaining({ - headers: { - Accept: "application/json", - Authorization: `Basic ${basic}`, - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - ) - }) - - test("it accepts custom auth url", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - url: "/custom/oauth", - }, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - - await client.fetch("http://example.com", { withAuth: true }) - expect(fetchSpy).toHaveBeenNthCalledWith( - 1, - `${BASE_URL}/custom/oauth`, - expect.anything() - ) - }) - - test("it throws an error if invalid auth is set", async () => { - expect( - () => - new DrupalClient(BASE_URL, { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - }, - }) - ).toThrow( - "'clientId' and 'clientSecret' are required for auth. See https://next-drupal.org/docs/client/auth" - ) - - expect(() => { - const client = new DrupalClient(BASE_URL) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - client.auth = { - clientSecret: "d92Fm^ds", - } - }).toThrow( - "'clientId' and 'clientSecret' are required for auth. See https://next-drupal.org/docs/client/auth" - ) - - expect( - () => - new DrupalClient(BASE_URL, { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - auth: { - username: "admin", - }, - }) - ).toThrow( - "'username' and 'password' are required for auth. See https://next-drupal.org/docs/client/auth" - ) - - expect( - () => - new DrupalClient(BASE_URL, { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - auth: { - password: "password", - }, - }) - ).toThrow( - "'username' and 'password' are required for auth. See https://next-drupal.org/docs/client/auth" - ) - }) -}) - -describe("getAccessToken", () => { - test("it fetches an access token", async () => { - jest.spyOn(global, "fetch").mockImplementation( - jest.fn(() => - Promise.resolve({ - ok: true, - json: () => - Promise.resolve({ - access_token: "ECYM594IlARGc3S8KgBHvTpki0rDtWx6", - token_type: "bearer", - expires_in: 3600, - }), - }) - ) as jest.Mock - ) - - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - }) - - const token = await client.getAccessToken() - expect(token).toEqual({ - access_token: "ECYM594IlARGc3S8KgBHvTpki0rDtWx6", - token_type: "bearer", - expires_in: 3600, - }) - }) - - test("it re-uses access token", async () => { - jest.spyOn(global, "fetch").mockImplementation( - jest.fn(() => - Promise.resolve({ - ok: true, - json: () => - Promise.resolve({ - access_token: "ECYM594IlARGc3S8KgBHvTpki0rDtWx6" + Math.random(), - token_type: "bearer", - expires_in: 3600, - }), - }) - ) as jest.Mock - ) - - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - }) - - const token1 = await client.getAccessToken() - const token2 = await client.getAccessToken() - expect(token1).toEqual(token2) - }) - - test("it accepts a long-lived accessToken", async () => { - const accessToken = { - token_type: "Bearer", - expires_in: 300, - access_token: - "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImVlNDkyOTI4ZTZjNj", - } - - const client = new DrupalClient(BASE_URL, { - accessToken, - }) - - const token = await client.getAccessToken() - - expect(token).toEqual(accessToken) - }) -}) - -describe("headers", () => { - test("it allows setting custom headers", async () => { - const customFetch = jest.fn() - const client = new DrupalClient(BASE_URL, { - fetcher: customFetch, - }) - client.headers = { - foo: "bar", - } - - const url = "http://example.com" - - await client.fetch(url) - expect(customFetch).toHaveBeenCalledWith( - url, - expect.objectContaining({ - headers: { foo: "bar" }, - }) - ) - }) - - test("it allows setting custom headers with custom auth", async () => { - const customFetch = jest.fn() - const client = new DrupalClient(BASE_URL, { - fetcher: customFetch, - headers: { - foo: "bar", - }, - auth: jest - .fn() - .mockReturnValue("Basic YXJzaGFkQG5leHQtZHJ1cGFsLm9yZzphYmMxMjM="), - }) - - const url = "http://example.com" - - await client.fetch(url, { withAuth: true }) - - expect(customFetch).toHaveBeenCalledWith( - url, - expect.objectContaining({ - headers: { - foo: "bar", - Authorization: "Basic YXJzaGFkQG5leHQtZHJ1cGFsLm9yZzphYmMxMjM=", - }, - withAuth: true, - }) - ) - }) -}) - -describe("logger", () => { - test("it allows custom logger", () => { - const logger: Logger = { - log(message) { - console.log(message) - }, - warn(message) { - console.warn(message) - }, - error(message) { - console.error(message) - }, - debug(message) { - console.debug(message) - }, - } - const debugSpy = jest.spyOn(logger, "debug").mockImplementation() - - new DrupalClient(BASE_URL, { debug: true, logger }) - - expect(debugSpy).toHaveBeenCalled() - }) -}) - -describe("fetch", () => { - test("it allows fetching custom url", async () => { - const client = new DrupalClient(BASE_URL) - const url = client.buildUrl( - "/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053" - ) - - const response = await client.fetch(url.toString()) - expect(response.headers.get("content-type")).toEqual( - "application/vnd.api+json" - ) - const json = await response.json() - expect(json).toMatchSnapshot() - }) - - test("it allows authenticated requests", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - }) - const url = client.buildUrl("/jsonapi") - - const getAccessTokenSpy = jest - .spyOn(client, "getAccessToken") - .mockImplementation() - - await client.fetch(url.toString(), { - withAuth: true, - }) - - expect(getAccessTokenSpy).toHaveBeenCalled() - }) - - test("it throws an error if withAuth is called when auth is not configured", async () => { - const client = new DrupalClient(BASE_URL) - - const url = client.buildUrl("/jsonapi") - - await expect( - client.fetch(url.toString(), { - withAuth: true, - }) - ).rejects.toThrow("auth is not configured.") - }) - - test("it allows for custom fetcher", async () => { - const customFetch = jest.fn() - - const client = new DrupalClient(BASE_URL, { - fetcher: customFetch, - }) - const url = client.buildUrl("/jsonapi").toString() - - await client.fetch(url) - expect(customFetch).toHaveBeenCalledWith( - url, - expect.objectContaining({ - headers: { - "Content-Type": "application/vnd.api+json", - Accept: "application/vnd.api+json", - }, - }) - ) - - await client.fetch(url, { - headers: { - foo: "bar", - }, - }) - expect(customFetch).toHaveBeenLastCalledWith( - url, - expect.objectContaining({ - headers: { - Accept: "application/vnd.api+json", - "Content-Type": "application/vnd.api+json", - foo: "bar", - }, - }) - ) - }) -}) - -describe("buildUrl", () => { - const client = new DrupalClient(BASE_URL) - - test("it builds a url", () => { - expect(client.buildUrl("http://example.com").toString()).toEqual( - "http://example.com/" - ) - }) - - test("it builds a relative url", () => { - expect(client.buildUrl("/foo").toString()).toEqual(`${BASE_URL}/foo`) - }) - - test("it builds a url with params", () => { - expect(client.buildUrl("/foo", { bar: "baz" }).toString()).toEqual( - `${BASE_URL}/foo?bar=baz` - ) - - expect( - client - .buildUrl("/jsonapi/node/article", { - sort: "-created", - "fields[node--article]": "title,path", - }) - .toString() - ).toEqual( - `${BASE_URL}/jsonapi/node/article?sort=-created&fields%5Bnode--article%5D=title%2Cpath` - ) - }) - - test("it builds a url from object (DrupalJsonApiParams)", () => { - const params = { - getQueryObject: () => ({ - sort: "-created", - "fields[node--article]": "title,path", - }), - } - - expect(client.buildUrl("/jsonapi/node/article", params).toString()).toEqual( - `${BASE_URL}/jsonapi/node/article?sort=-created&fields%5Bnode--article%5D=title%2Cpath` - ) - }) -}) - -describe("deserialize", () => { - test("it deserializes JSON:API resource", async () => { - const client = new DrupalClient(BASE_URL) - const url = client.buildUrl( - "/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053", - { - include: "field_tags", - } - ) - - const response = await client.fetch(url.toString()) - const json = await response.json() - const article = client.deserialize(json) as DrupalNode - - expect(article).toMatchSnapshot() - expect(article.id).toEqual("52837ad0-f218-46bd-a106-5710336b7053") - expect(article.field_tags).toHaveLength(3) - }) - - test("it deserializes JSON:API collection", async () => { - const client = new DrupalClient(BASE_URL) - const url = client.buildUrl("/jsonapi/node/article", { - getQueryObject: () => ({ - "fields[node--article]": "title", - }), - }) - - const response = await client.fetch(url.toString()) - const json = await response.json() - const articles = client.deserialize(json) as DrupalNode[] - - expect(articles).toMatchSnapshot() - }) - - test("it allows for custom data serializer", async () => { - const serializer: Serializer = { - deserialize: ( - body: { data: { id: string; attributes: { title: string } } }, - options: { pathPrefix: string } - ) => { - return { - id: body.data.id, - title: `${options.pathPrefix}: ${body.data.attributes.title}`, - } - }, - } - const client = new DrupalClient(BASE_URL, { - serializer, - }) - const url = client.buildUrl( - "/jsonapi/node/article/52837ad0-f218-46bd-a106-5710336b7053" - ) - - const response = await client.fetch(url.toString()) - const json = await response.json() - const article = client.deserialize(json, { - pathPrefix: "TITLE", - }) as DrupalNode - - expect(article).toMatchSnapshot() - expect(article.id).toEqual("52837ad0-f218-46bd-a106-5710336b7053") - expect(article.title).toEqual(`TITLE: ${json.data.attributes.title}`) - }) -}) - -describe("getPathFromContext", () => { - test("it returns a path from context", async () => { - const client = new DrupalClient(BASE_URL) - - expect( - client.getPathFromContext({ - params: { - slug: ["foo"], - }, - }) - ).toEqual("/foo") - - expect( - client.getPathFromContext({ - params: { - slug: ["foo", "bar"], - }, - }) - ).toEqual("/foo/bar") - - expect( - client.getPathFromContext({ - locale: "en", - defaultLocale: "es", - params: { - slug: ["foo", "bar"], - }, - }) - ).toEqual("/en/foo/bar") - - expect( - client.getPathFromContext({ - params: { - slug: [], - }, - }) - ).toEqual("/home") - - client.frontPage = "/front" - - expect( - client.getPathFromContext({ - params: { - slug: [], - }, - }) - ).toEqual("/front") - - expect( - client.getPathFromContext({ - locale: "es", - defaultLocale: "en", - params: { - slug: [], - }, - }) - ).toEqual("/es/front") - }) - - test("it returns a path from context with pathPrefix", () => { - const client = new DrupalClient(BASE_URL) - - expect( - client.getPathFromContext( - { - params: { - slug: ["bar", "baz"], - }, - }, - { - pathPrefix: "/foo", - } - ) - ).toEqual("/foo/bar/baz") - - expect( - client.getPathFromContext( - { - params: { - slug: ["bar", "baz"], - }, - }, - { - pathPrefix: "foo", - } - ) - ).toEqual("/foo/bar/baz") - - expect( - client.getPathFromContext( - { - locale: "en", - defaultLocale: "en", - params: { - slug: ["bar", "baz"], - }, - }, - { - pathPrefix: "foo", - } - ) - ).toEqual("/foo/bar/baz") - - expect( - client.getPathFromContext( - { - locale: "es", - defaultLocale: "en", - params: { - slug: ["bar", "baz"], - }, - }, - { - pathPrefix: "foo", - } - ) - ).toEqual("/es/foo/bar/baz") - - expect( - client.getPathFromContext( - { - locale: "es", - defaultLocale: "en", - params: { - slug: [], - }, - }, - { - pathPrefix: "/foo", - } - ) - ).toEqual("/es/foo/home") - - client.frontPage = "/baz" - - expect( - client.getPathFromContext( - { - locale: "en", - defaultLocale: "en", - params: { - slug: [], - }, - }, - { - pathPrefix: "foo", - } - ) - ).toEqual("/foo/baz") - - expect( - client.getPathFromContext( - { - params: { - slug: [], - }, - }, - { - pathPrefix: "/foo/bar", - } - ) - ).toEqual("/foo/bar/baz") - }) - - test("it encodes path with punctuation", async () => { - const client = new DrupalClient(BASE_URL) - - const path = client.getPathFromContext({ - params: { - slug: ["path&with^punc&in$path"], - }, - }) - - expect(path).toEqual("/path%26with%5Epunc%26in%24path") - - const translatedPath = await client.translatePath(path) - - expect(translatedPath).toMatchSnapshot() - }) -}) - -describe("getIndex", () => { - test("it fetches the JSON:API index", async () => { - const client = new DrupalClient(BASE_URL) - const index = await client.getIndex() - - expect(index).toMatchSnapshot() - }) - - test("it fetches the JSON:API index with locale", async () => { - const client = new DrupalClient(BASE_URL) - const index = await client.getIndex("es") - - expect(index).toMatchSnapshot() - }) - - test("it throws error for invalid base url", async () => { - const client = new DrupalClient("https://example.com") - - await expect(client.getIndex()).rejects.toThrow( - "Failed to fetch JSON:API index at https://example.com/jsonapi" - ) - }) -}) - -describe("getEntryForResourceType", () => { - test("it returns the JSON:API entry for a resource type", async () => { - const client = new DrupalClient(BASE_URL) - const getIndexSpy = jest.spyOn(client, "getIndex") - - const recipeEntry = await client.getEntryForResourceType("node--recipe") - expect(recipeEntry).toMatch(`${BASE_URL}/en/jsonapi/node/recipe`) - expect(getIndexSpy).toHaveBeenCalledTimes(1) - - const articleEntry = await client.getEntryForResourceType("node--article") - expect(articleEntry).toMatch(`${BASE_URL}/en/jsonapi/node/article`) - expect(getIndexSpy).toHaveBeenCalledTimes(2) - }) - - test("it assembles JSON:API entry without fetching index", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - }) - const getIndexSpy = jest.spyOn(client, "getIndex") - - const recipeEntry = await client.getEntryForResourceType("node--article") - expect(recipeEntry).toMatch(`${BASE_URL}/jsonapi/node/article`) - expect(getIndexSpy).toHaveBeenCalledTimes(0) - }) - - test("it throws an error if resource type does not exist", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getEntryForResourceType("RESOURCE-DOES-NOT-EXIST") - ).rejects.toThrow("Resource of type 'RESOURCE-DOES-NOT-EXIST' not found.") - }) -}) - -describe("getResource", () => { - test("it fetches a resource by uuid", async () => { - const client = new DrupalClient(BASE_URL) - const recipe = await client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f" - ) - - expect(recipe).toMatchSnapshot() - }) - - test("it fetches a resource by uuid with params", async () => { - const client = new DrupalClient(BASE_URL) - const recipe = await client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - { - params: { - "fields[node--recipe]": "title,field_cooking_time", - }, - } - ) - - expect(recipe).toMatchSnapshot() - }) - - test("it fetches a resource using locale", async () => { - const client = new DrupalClient(BASE_URL) - const recipe = await client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - { - locale: "es", - defaultLocale: "en", - params: { - "fields[node--recipe]": "title,field_cooking_time", - }, - } - ) - - expect(recipe).toMatchSnapshot() - }) - - test("it fetches raw data", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - { - deserialize: false, - } - ) - ).resolves.toMatchSnapshot() - }) - - test("it fetches a resource by revision", async () => { - const client = new DrupalClient(BASE_URL) - const recipe = await client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - { - params: { - "fields[node--recipe]": "drupal_internal__vid", - }, - } - ) - const latestRevision = await client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - { - params: { - resourceVersion: "rel:latest-version", - "fields[node--recipe]": "drupal_internal__vid", - }, - } - ) - - expect(recipe.drupal_internal__vid).toEqual( - latestRevision.drupal_internal__vid - ) - }) - - test("it throws an error for invalid revision", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - { - params: { - resourceVersion: "id:-11", - "fields[node--recipe]": "title", - }, - } - ) - ).rejects.toThrow( - "404 Not Found\nThe requested version, identified by `id:-11`, could not be found." - ) - }) - - test("it throws an error if revision access is forbidden", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - { - params: { - resourceVersion: "id:1", - "fields[node--recipe]": "title", - }, - } - ) - ).rejects.toThrow( - "401 Unauthorized\nNo authentication credentials provided." - ) - }) - - test("it throws an error for invalid resource type", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResource( - "RESOURCE-DOES-NOT-EXIST", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f" - ) - ).rejects.toThrow("Resource of type 'RESOURCE-DOES-NOT-EXIST' not found.") - }) - - test("it throws an error for invalid params", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - { - params: { - include: "invalid_relationship", - }, - } - ) - ).rejects.toThrow( - "400 Bad Request\n`invalid_relationship` is not a valid relationship field name. Possible values: node_type, revision_uid, uid, menu_link, field_media_image, field_recipe_category, field_tags." - ) - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - - await client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f" - ) - expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { - withAuth: false, - }) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - await client.getResource( - "node--recipe", - "71e04ead-4cc7-416c-b9ca-60b635fdc50f", - { - withAuth: true, - } - ) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) -}) - -describe("getResourceByPath", () => { - test("it fetches a resource by path", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResourceByPath("/recipes/deep-mediterranean-quiche") - ).resolves.toMatchSnapshot() - }) - - test("it fetches a resource by path with params", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResourceByPath("/recipes/deep-mediterranean-quiche", { - params: { - "fields[node--recipe]": "title,field_cooking_time", - }, - }) - ).resolves.toMatchSnapshot() - }) - - test("it fetches a resource by path using locale", async () => { - const client = new DrupalClient(BASE_URL) - const recipe = await client.getResourceByPath( - "/recipes/quiche-mediterráneo-profundo", - { - locale: "es", - defaultLocale: "en", - params: { - "fields[node--recipe]": "title,field_cooking_time", - }, - } - ) - - expect(recipe).toMatchSnapshot() - }) - - test("it fetches raw data", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResourceByPath("/recipes/deep-mediterranean-quiche", { - deserialize: false, - }) - ).resolves.toMatchSnapshot() - }) - - test("it fetches a resource by revision", async () => { - const client = new DrupalClient(BASE_URL) - const recipe = await client.getResourceByPath( - "/recipes/deep-mediterranean-quiche", - { - params: { - "fields[node--recipe]": "drupal_internal__vid", - }, - } - ) - const latestRevision = await client.getResourceByPath( - "/recipes/deep-mediterranean-quiche", - { - params: { - resourceVersion: "rel:latest-version", - "fields[node--recipe]": "drupal_internal__vid", - }, - } - ) - - expect(recipe.drupal_internal__vid).toEqual( - latestRevision.drupal_internal__vid - ) - }) - - test("it throws an error for invalid revision", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResourceByPath( - "/recipes/deep-mediterranean-quiche", - { - params: { - resourceVersion: "id:-11", - "fields[node--recipe]": "title", - }, - } - ) - ).rejects.toThrow( - "404 Not Found\nThe requested version, identified by `id:-11`, could not be found." - ) - }) - - test("it throws an error if revision access is forbidden", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResourceByPath( - "/recipes/deep-mediterranean-quiche", - { - params: { - resourceVersion: "id:1", - "fields[node--recipe]": "title", - }, - } - ) - ).rejects.toThrow( - "401 Unauthorized\nNo authentication credentials provided." - ) - }) - - test("it returns null for path not found", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResourceByPath("/path-do-not-exist") - ).rejects.toThrow("Unable to resolve path /path-do-not-exist.") - }) - - test("it throws an error for invalid params", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResourceByPath( - "/recipes/deep-mediterranean-quiche", - { - params: { - include: "invalid_relationship", - }, - } - ) - ).rejects.toThrow( - "400 Bad Request\n`invalid_relationship` is not a valid relationship field name. Possible values: node_type, revision_uid, uid, menu_link, field_media_image, field_recipe_category, field_tags." - ) - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - const getAccessTokenSpy = jest.spyOn(client, "getAccessToken") - - await client.getResourceByPath( - "/recipes/deep-mediterranean-quiche" - ) - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.not.objectContaining({ - withAuth: true, - }) - ) - expect(getAccessTokenSpy).not.toHaveBeenCalled() - }) - - test("it makes authenticated requests with withAuth", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - const getAccessTokenSpy = jest.spyOn(client, "getAccessToken") - - await client.getResourceByPath( - "/recipes/deep-mediterranean-quiche", - { - withAuth: true, - } - ) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - expect(getAccessTokenSpy).toHaveBeenCalled() - }) -}) - -describe("getResourceFromContext", () => { - test("it fetches a resource from context", async () => { - const client = new DrupalClient(BASE_URL) - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - const recipe = await client.getResourceFromContext( - "node--recipe", - context - ) - - expect(recipe).toMatchSnapshot() - }) - - test("it fetches a resource from context with params", async () => { - const client = new DrupalClient(BASE_URL) - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - const recipe = await client.getResourceFromContext( - "node--recipe", - context, - { - params: { - "fields[node--recipe]": "title", - }, - } - ) - - expect(recipe).toMatchSnapshot() - }) - - test("it fetches a resource from context using locale", async () => { - const client = new DrupalClient(BASE_URL) - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "quiche-mediterráneo-profundo"], - }, - locale: "es", - defaultLocale: "en", - } - const recipe = await client.getResourceFromContext( - "node--recipe", - context, - { - params: { - "fields[node--recipe]": "title,field_cooking_time", - }, - } - ) - - expect(recipe).toMatchSnapshot() - }) - - test("it fetches raw data", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - const recipe = await client.getResourceFromContext( - "node--recipe", - context, - { - deserialize: false, - params: { - "fields[node--recipe]": "title", - }, - } - ) - - expect(recipe).toMatchSnapshot() - }) - - test("it fetches a resource from context by revision", async () => { - const client = new DrupalClient(BASE_URL) - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "quiche-mediterráneo-profundo"], - }, - locale: "es", - defaultLocale: "en", - } - const recipe = await client.getResourceFromContext( - "node--recipe", - context, - { - params: { - "fields[node--recipe]": "drupal_internal__vid", - }, - } - ) - - context.previewData = { resourceVersion: "rel:latest-version" } - - const latestRevision = await client.getResourceFromContext( - "node--recipe", - context, - { - params: { - "fields[node--recipe]": "drupal_internal__vid", - }, - } - ) - - expect(recipe.drupal_internal__vid).toEqual( - latestRevision.drupal_internal__vid - ) - }) - - test("it throws an error for invalid revision", async () => { - const client = new DrupalClient(BASE_URL) - const context: GetStaticPropsContext = { - previewData: { - resourceVersion: "id:-11", - }, - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - - await expect( - client.getResourceFromContext("node--recipe", context, { - params: { - "fields[node--recipe]": "drupal_internal__vid", - }, - }) - ).rejects.toThrow( - "404 Not Found\nThe requested version, identified by `id:-11`, could not be found." - ) - }) - - test("it throws an error if revision access is forbidden", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - previewData: { - resourceVersion: "id:1", - }, - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - - await expect( - client.getResourceFromContext("node--recipe", context, { - params: { - "fields[node--recipe]": "title", - }, - }) - ).rejects.toThrow( - "401 Unauthorized\nNo authentication credentials provided." - ) - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - - await client.getResourceFromContext("node--recipe", context) - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: false, - }) - ) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - - await client.getResourceFromContext("node--recipe", context, { - withAuth: true, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) - - test("it makes authenticated requests when preview is true", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - const context: GetStaticPropsContext = { - preview: true, - previewData: { - plugin: "simple_oauth", - scope: "editor", - }, - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - - await client.getResourceFromContext("node--recipe", context) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: `Bearer sample-token`, - }) - ) - }) - - test("it accepts a translated path", async () => { - const client = new DrupalClient(BASE_URL) - - const path = await client.translatePath("recipes/deep-mediterranean-quiche") - - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - - if (path) { - const recipe = await client.getResourceFromContext(path, context, { - params: { - "fields[node--recipe]": "title,path,status", - }, - }) - - await expect(recipe).toMatchSnapshot() - } - }) -}) - -describe("translatePath", () => { - test("it translates a path", async () => { - const client = new DrupalClient(BASE_URL) - - const path = await client.translatePath("recipes/deep-mediterranean-quiche") - - expect(path).toMatchSnapshot() - - const path2 = await client.translatePath( - "/recipes/deep-mediterranean-quiche" - ) - - expect(path).toEqual(path2) - }) - - test("it returns null for path not found", async () => { - const client = new DrupalClient(BASE_URL) - - const path = await client.translatePath("/path-not-found") - - expect(path).toBeNull() - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - - await client.translatePath("recipes/deep-mediterranean-quiche") - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: false, - }) - ) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - await client.translatePath("recipes/deep-mediterranean-quiche", { - withAuth: true, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) -}) - -describe("translatePathFromContext", () => { - test("it translates a path", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - - const path = await client.translatePathFromContext(context) - - expect(path).toMatchSnapshot() - }) - - test("it returns null for path not found", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - params: { - slug: ["path-not-found"], - }, - } - - const path = await client.translatePathFromContext(context) - - expect(path).toBeNull() - }) - - test("it translates a path with pathPrefix", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - params: { - slug: ["deep-mediterranean-quiche"], - }, - } - - const path = await client.translatePathFromContext(context, { - pathPrefix: "recipes", - }) - - expect(path).toMatchSnapshot() - - const path2 = await client.translatePathFromContext(context, { - pathPrefix: "/recipes", - }) - - expect(path).toEqual(path2) - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - - const context: GetStaticPropsContext = { - params: { - slug: ["recipes", "deep-mediterranean-quiche"], - }, - } - await client.translatePathFromContext(context) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: false, - }) - ) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - const context: GetStaticPropsContext = { - params: { - slug: ["deep-mediterranean-quiche"], - }, - } - await client.translatePathFromContext(context, { - pathPrefix: "recipes", - withAuth: true, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) -}) - -describe("getResourceCollection", () => { - test("it fetches a resource collection", async () => { - const client = new DrupalClient(BASE_URL) - - const articles = await client.getResourceCollection("node--article", { - params: { - "fields[node--article]": "title", - }, - }) - - expect(articles).toMatchSnapshot() - }) - - test("it fetches a resource collection using locale", async () => { - const client = new DrupalClient(BASE_URL) - - const articles = await client.getResourceCollection("node--article", { - locale: "es", - defaultLocale: "en", - params: { - "fields[node--article]": "title,langcode", - }, - }) - - expect(articles[0].langcode).toEqual("es") - - expect(articles).toMatchSnapshot() - }) - - test("it fetches raw data", async () => { - const client = new DrupalClient(BASE_URL) - - const recipes = await client.getResourceCollection("node--recipe", { - deserialize: false, - params: { - "fields[node--recipe]": "title", - "page[limit]": 2, - }, - }) - - expect(recipes).toMatchSnapshot() - }) - - test("it throws an error for invalid resource type", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResourceCollection("RESOURCE-DOES-NOT-EXIST") - ).rejects.toThrow("Resource of type 'RESOURCE-DOES-NOT-EXIST' not found.") - }) - - test("it throws an error for invalid params", async () => { - const client = new DrupalClient(BASE_URL) - - await expect( - client.getResourceCollection("node--recipe", { - params: { - include: "invalid_relationship", - }, - }) - ).rejects.toThrow( - "400 Bad Request\n`invalid_relationship` is not a valid relationship field name. Possible values: node_type, revision_uid, uid, menu_link, field_media_image, field_recipe_category, field_tags." - ) - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - - await client.getResourceCollection("node--recipe") - expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { - withAuth: false, - }) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - await client.getResourceCollection("node--recipe", { - withAuth: true, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) -}) - -describe("getResourceCollectionFromContext", () => { - test("it fetches a resource collection", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - locale: "en", - defaultLocale: "en", - } - - const articles = await client.getResourceCollectionFromContext( - "node--article", - context, - { - params: { - "fields[node--article]": "title", - }, - } - ) - - expect(articles).toMatchSnapshot() - }) - - test("it fetches a resource collection using locale", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - locale: "es", - defaultLocale: "en", - } - - const articles = await client.getResourceCollectionFromContext( - "node--article", - context, - { - params: { - "fields[node--article]": "title,langcode", - }, - } - ) - - expect(articles[0].langcode).toEqual("es") - - expect(articles).toMatchSnapshot() - }) - - test("it fetches raw data", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - locale: "en", - defaultLocale: "en", - } - - const recipes = await client.getResourceCollectionFromContext( - "node--recipe", - context, - { - deserialize: false, - params: { - "fields[node--recipe]": "title", - "page[limit]": 2, - }, - } - ) - - expect(recipes).toMatchSnapshot() - }) - - test("it throws an error for invalid resource type", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - locale: "en", - defaultLocale: "en", - } - - await expect( - client.getResourceCollectionFromContext( - "RESOURCE-DOES-NOT-EXIST", - context - ) - ).rejects.toThrow("Resource of type 'RESOURCE-DOES-NOT-EXIST' not found.") - }) - - test("it throws an error for invalid params", async () => { - const client = new DrupalClient(BASE_URL) - - const context: GetStaticPropsContext = { - locale: "en", - defaultLocale: "en", - } - - await expect( - client.getResourceCollectionFromContext( - "node--recipe", - context, - { - params: { - include: "invalid_relationship", - }, - } - ) - ).rejects.toThrow( - "400 Bad Request\n`invalid_relationship` is not a valid relationship field name. Possible values: node_type, revision_uid, uid, menu_link, field_media_image, field_recipe_category, field_tags." - ) - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - - const context: GetStaticPropsContext = { - locale: "en", - defaultLocale: "en", - } - - await client.getResourceCollectionFromContext("node--recipe", context) - expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { - withAuth: false, - }) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - const context: GetStaticPropsContext = { - locale: "en", - defaultLocale: "en", - } - await client.getResourceCollectionFromContext("node--recipe", context, { - withAuth: true, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) -}) - -describe("getStaticPathsFromContext", () => { - test("it returns static paths from context", async () => { - const client = new DrupalClient(BASE_URL) - - const paths = await client.getStaticPathsFromContext("node--article", {}) - - expect(paths).toMatchSnapshot() - }) - - test("it returns static paths from context with locale", async () => { - const client = new DrupalClient(BASE_URL) - - const paths = await client.getStaticPathsFromContext("node--article", { - locales: ["en", "es"], - defaultLocale: "en", - }) - - expect(paths).toMatchSnapshot() - }) - - test("it returns static paths for multiple resource types from context", async () => { - const client = new DrupalClient(BASE_URL) - - const paths = await client.getStaticPathsFromContext( - ["node--article", "node--recipe"], - { - locales: ["en", "es"], - defaultLocale: "en", - } - ) - - expect(paths).toMatchSnapshot() - }) - - test("it returns static paths from context with params", async () => { - const client = new DrupalClient(BASE_URL) - - const paths = await client.getStaticPathsFromContext( - "node--article", - {}, - { - params: { - "filter[promote]": 1, - }, - } - ) - - expect(paths).toMatchSnapshot() - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - - await client.getStaticPathsFromContext("node--article", { - locales: ["en", "es"], - defaultLocale: "en", - }) - expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { - withAuth: false, - }) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - await client.getStaticPathsFromContext( - "node--article", - { - locales: ["en", "es"], - defaultLocale: "en", - }, - { - withAuth: true, - } - ) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) -}) - -describe("buildStaticPathsParamsFromPaths", () => { - test("it builds static paths from paths", () => { - const client = new DrupalClient(BASE_URL) - - const paths = ["/blog/post/one", "/blog/post/two", "/blog/post/three"] - - expect(client.buildStaticPathsParamsFromPaths(paths)).toMatchSnapshot() - - expect( - client.buildStaticPathsParamsFromPaths(paths, { locale: "en" }) - ).toMatchSnapshot() - }) - - test("it builds static paths from paths with pathPrefix", () => { - const client = new DrupalClient(BASE_URL) - - const paths = client.buildStaticPathsParamsFromPaths( - ["/blog/post/one", "/blog/post/two", "/blog/post"], - { pathPrefix: "blog" } - ) - - const paths2 = client.buildStaticPathsParamsFromPaths( - ["/blog/post/one", "/blog/post/two", "/blog/post"], - { pathPrefix: "/blog" } - ) - - const paths3 = client.buildStaticPathsParamsFromPaths( - ["blog/post/one", "blog/post/two", "blog/post"], - { pathPrefix: "/blog" } - ) - - const paths4 = client.buildStaticPathsParamsFromPaths( - ["blog/post/one", "blog/post/two", "blog/post"], - { pathPrefix: "blog" } - ) - - expect(paths).toMatchSnapshot() - - expect(paths).toEqual(paths2) - expect(paths).toEqual(paths3) - expect(paths).toEqual(paths4) - }) -}) - -describe("buildStaticPathsFromResources", () => { - test("it builds static paths from resources", () => { - const client = new DrupalClient(BASE_URL) - - const resources: Pick[] = [ - { - path: { - alias: "blog/post/one", - pid: 1, - langcode: "en", - }, - }, - { - path: { - alias: "blog/post/two", - pid: 2, - langcode: "en", - }, - }, - ] - - expect(client.buildStaticPathsFromResources(resources)).toMatchSnapshot() - - expect( - client.buildStaticPathsFromResources(resources, { locale: "es" }) - ).toMatchSnapshot() - }) - - test("it builds static paths from resources with pathPrefix", () => { - const client = new DrupalClient(BASE_URL) - - const resources: Pick[] = [ - { - path: { - alias: "blog/post/one", - pid: 1, - langcode: "en", - }, - }, - { - path: { - alias: "blog/post/two", - pid: 2, - langcode: "en", - }, - }, - ] - - const paths = client.buildStaticPathsFromResources(resources, { - pathPrefix: "blog", - }) - - const paths2 = client.buildStaticPathsFromResources(resources, { - pathPrefix: "/blog", - }) - - const paths3 = client.buildStaticPathsFromResources(resources, { - pathPrefix: "/blog/post", - locale: "es", - }) - - const paths4 = client.buildStaticPathsFromResources(resources, { - pathPrefix: "blog/post", - locale: "es", - }) - - expect(paths).toMatchSnapshot() - expect(paths3).toMatchSnapshot() - - expect(paths).toEqual(paths2) - expect(paths3).toEqual(paths4) - }) -}) - -describe("getMenu", () => { - test("it fetches menu items for a menu", async () => { - const client = new DrupalClient(BASE_URL) - - const menu = await client.getMenu("main") - - expect(menu).toMatchSnapshot() - }) - - test("it fetches menu items for a menu with locale", async () => { - const client = new DrupalClient(BASE_URL) - - const menu = await client.getMenu("main", { - locale: "es", - defaultLocale: "en", - }) - - expect(menu).toMatchSnapshot() - }) - - test("it fetches menu items for a menu with params", async () => { - const client = new DrupalClient(BASE_URL) - - const menu = await client.getMenu("main", { - params: { - "fields[menu_link_content--menu_link_content]": "title", - }, - }) - - expect(menu).toMatchSnapshot() - }) - - test("it throws an error for invalid menu name", async () => { - const client = new DrupalClient(BASE_URL) - - await expect(client.getMenu("INVALID")).rejects.toThrow( - '404 Not Found\nThe "menu" parameter was not converted for the path "/jsonapi/menu_items/{menu}" (route name: "jsonapi_menu_items.menu")' - ) - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - - await client.getMenu("main") - expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { - withAuth: false, - }) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - await client.getMenu("main", { withAuth: true }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) -}) - -describe("getView", () => { - test("it fetches a view", async () => { - const client = new DrupalClient(BASE_URL) - - const view = await client.getView("featured_articles--page_1") - - expect(view).toMatchSnapshot() - }) - - test("it fetches a view with params", async () => { - const client = new DrupalClient(BASE_URL) - - const view = await client.getView("featured_articles--page_1", { - params: { - "fields[node--article]": "title", - }, - }) - - expect(view).toMatchSnapshot() - }) - - test("it fetches a view with locale", async () => { - const client = new DrupalClient(BASE_URL) - - const view = await client.getView("featured_articles--page_1", { - locale: "es", - defaultLocale: "en", - params: { - "fields[node--article]": "title", - }, - }) - - expect(view).toMatchSnapshot() - }) - - test("it fetches raw data", async () => { - const client = new DrupalClient(BASE_URL) - - const view = await client.getView("featured_articles--page_1", { - locale: "es", - defaultLocale: "en", - deserialize: false, - params: { - "fields[node--article]": "title", - }, - }) - - expect(view).toMatchSnapshot() - }) - - test("it throws an error for invalid view name", async () => { - const client = new DrupalClient(BASE_URL) - - await expect(client.getView("INVALID")).rejects.toThrow("Not Found") - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - - await client.getView("featured_articles--page_1") - expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { - withAuth: false, - }) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - await client.getView("featured_articles--page_1", { withAuth: true }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) - - test("it fetches a view with links for pagination", async () => { - const client = new DrupalClient(BASE_URL) - const view = await client.getView("recipes--page_1") - - expect(view.links).toHaveProperty("next") - }) -}) - -describe("getSearchIndex", () => { - test("it fetches a search index", async () => { - const client = new DrupalClient(BASE_URL) - - const search = await client.getSearchIndex("recipes", { - params: { - "fields[node--recipe]": "title", - }, - }) - - expect(search).toMatchSnapshot() - }) - - test("it fetches a search index with locale", async () => { - const client = new DrupalClient(BASE_URL) - - const search = await client.getSearchIndex("recipes", { - locale: "es", - defaultLocale: "en", - params: { - "fields[node--recipe]": "title", - }, - }) - - expect(search).toMatchSnapshot() - }) - - test("it fetches a search index with facets filters", async () => { - const client = new DrupalClient(BASE_URL) - - const search = await client.getSearchIndex( - "recipes", - { - deserialize: false, - params: { - "filter[difficulty]": "easy", - "fields[node--recipe]": "title,field_difficulty", - }, - } - ) - - expect(search).toMatchSnapshot() - expect(search.meta.facets).not.toBeNull() - }) - - test("it fetches raw data from search index", async () => { - const client = new DrupalClient(BASE_URL) - - const search = await client.getSearchIndex("recipes", { - deserialize: false, - params: { - "filter[difficulty]": "easy", - "fields[node--recipe]": "title,field_difficulty", - }, - }) - - expect(search).toMatchSnapshot() - }) - - test("it makes un-authenticated requests by default", async () => { - const client = new DrupalClient(BASE_URL) - const fetchSpy = jest.spyOn(client, "fetch") - - await client.getSearchIndex("recipes") - - expect(fetchSpy).toHaveBeenCalledWith(expect.anything(), { - withAuth: false, - }) - }) - - test("it throws an error for invalid index", async () => { - const client = new DrupalClient(BASE_URL) - - await expect(client.getSearchIndex("INVALID-INDEX")).rejects.toThrow( - "Not Found" - ) - }) - - test("it makes authenticated requests with withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - useDefaultResourceTypeEntry: true, - auth: `Bearer sample-token`, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - await client.getSearchIndex("recipes", { - withAuth: true, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) -}) - -describe("getAuthFromContextAndOptions", () => { - test("if NOT in preview and withAuth option is provided, it should use the withAuth option", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - jest.spyOn(client, "getAccessToken").mockImplementation() - - await client.getResourceFromContext( - "node--article", - { - preview: false, - }, - { - withAuth: true, - } - ) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - - await client.getResourceFromContext( - "node--article", - { - preview: false, - }, - { - withAuth: { - clientId: "foo", - clientSecret: "bar", - scope: "baz", - }, - } - ) - - expect(fetchSpy).toHaveBeenLastCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: { - clientId: "foo", - clientSecret: "bar", - scope: "baz", - }, - }) - ) - }) - - test("if NOT in preview and no withAuth option provided, it should fallback to the global auth", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - - await client.getResourceFromContext("node--article", { - preview: false, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: false, - }) - ) - - const client2 = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - withAuth: true, - }) - jest.spyOn(client2, "getAccessToken").mockImplementation() - - await client2.getResourceFromContext("node--article", { - preview: false, - }) - - expect(fetchSpy).toHaveBeenLastCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: true, - }) - ) - }) - - test("if in preview, it should NOT use the global auth", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - withAuth: true, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - - await client.getResourceFromContext("node--article", { - preview: true, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: null, - }) - ) - }) - - test("if in preview and using the simple_oauth plugin, it should use the scope from context", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - - await client.getResourceFromContext("node--article", { - preview: true, - previewData: { - plugin: "simple_oauth", - scope: "editor", - }, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - scope: "editor", - url: "/oauth/token", - }, - }) - ) - }) - - test("if in preview and using the simple_oauth plugin, tt should use the scope from context even with global withAuth", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - scope: "administrator", - }, - withAuth: true, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - - await client.getResourceFromContext("node--article", { - preview: true, - previewData: { - plugin: "simple_oauth", - scope: "editor", - }, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - scope: "editor", - url: "/oauth/token", - }, - }) - ) - }) - - test("if in preview and using the jwt plugin, it should use the access_token from context", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - }, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - - await client.getResourceFromContext("node--article", { - preview: true, - previewData: { - plugin: "jwt", - access_token: "example-token", - }, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: `Bearer example-token`, - }) - ) - }) - - test("if in preview and using the jwt plugin, it should use the access token from context even with global withAuth", async () => { - const client = new DrupalClient(BASE_URL, { - auth: { - clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", - clientSecret: "d92Fm^ds", - scope: "administrator", - }, - withAuth: true, - }) - const fetchSpy = jest - .spyOn(global, "fetch") - .mockImplementation( - jest.fn(() => - Promise.resolve({ ok: true, json: () => Promise.resolve({}) }) - ) as jest.Mock - ) - - await client.getResourceFromContext("node--article", { - preview: true, - previewData: { - plugin: "jwt", - access_token: "example-token", - }, - }) - - expect(fetchSpy).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - withAuth: `Bearer example-token`, - }) - ) - }) -}) diff --git a/packages/next-drupal/tests/draft/draft.test.ts b/packages/next-drupal/tests/draft/draft.test.ts new file mode 100644 index 00000000..10499087 --- /dev/null +++ b/packages/next-drupal/tests/draft/draft.test.ts @@ -0,0 +1,206 @@ +import { + afterEach, + beforeEach, + describe, + expect, + jest, + test, +} from "@jest/globals" +import { cookies, draftMode } from "next/headers" +import { redirect } from "next/navigation" +import { NextRequest } from "next/server" +import { + DRAFT_DATA_COOKIE_NAME, + DRAFT_MODE_COOKIE_NAME, + DrupalClient, +} from "../../src" +import { BASE_URL, spyOnFetch } from "../utils" +import { + disableDraftMode, + enableDraftMode, + getDraftData, +} from "../../src/draft" +import { resetNextHeaders } from "../__mocks__/next/headers" +import type { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies" + +jest.mock("next/headers") +jest.mock("next/navigation", () => ({ + redirect: jest.fn(), +})) + +beforeEach(() => { + resetNextHeaders() +}) + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe("enableDraftMode()", () => { + const searchParams = new URLSearchParams({ + slug: "/example", + resourceVersion: "id:1", + plugin: "simple_oauth", + secret: "very-secret-key", + }) + const validationPayload = { + path: "/example", + maxAge: 30, + } + const request = new NextRequest( + `https://example.com/api/draft?${searchParams}` + ) + const client = new DrupalClient(BASE_URL) + const draftModeCookie: ResponseCookie = { + name: DRAFT_MODE_COOKIE_NAME, + value: "some-secret-key", + sameSite: "lax", + } + + test("does not enable draft mode if validation fails", async () => { + spyOnFetch({ responseBody: { message: "fail" }, status: 500 }) + + const response = await enableDraftMode(request, client) + + expect(draftMode().enable).not.toHaveBeenCalled() + expect(response).toBeInstanceOf(Response) + expect(response.status).toBe(500) + }) + + test("enables draft mode", async () => { + spyOnFetch({ responseBody: validationPayload }) + + await enableDraftMode(request, client) + + expect(draftMode().enable).toHaveBeenCalled() + }) + + test("updates draft mode cookie’s sameSite flag", async () => { + spyOnFetch({ responseBody: validationPayload }) + + // Our mock draftMode().enable does not set a cookie, so we set one. + cookies().set(draftModeCookie) + expect(cookies().get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("lax") + expect(cookies().get(DRAFT_MODE_COOKIE_NAME).secure).toBeFalsy() + + await enableDraftMode(request, client) + + expect(cookies().get(DRAFT_MODE_COOKIE_NAME).sameSite).toBe("none") + expect(cookies().get(DRAFT_MODE_COOKIE_NAME).secure).toBe(true) + }) + + test("sets a draft data cookie", async () => { + spyOnFetch({ responseBody: validationPayload }) + expect(cookies().get(DRAFT_DATA_COOKIE_NAME)).toBe(undefined) + + await enableDraftMode(request, client) + + const cookie = cookies().get(DRAFT_DATA_COOKIE_NAME) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { secret, plugin, ...data } = Object.fromEntries( + searchParams.entries() + ) + expect(cookie).toMatchObject({ + name: DRAFT_DATA_COOKIE_NAME, + sameSite: "none", + secure: true, + value: JSON.stringify(data), + }) + }) + + test("redirects to the slug path", async () => { + spyOnFetch({ responseBody: validationPayload }) + + await enableDraftMode(request, client) + + expect(redirect).toHaveBeenCalledWith(searchParams.get("slug")) + }) +}) + +describe("disableDraftMode()", () => { + test("draft data cookie was deleted", () => { + disableDraftMode() + + expect(cookies).toHaveBeenCalledTimes(1) + expect(cookies().delete).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME) + }) + + test("draft mode was disabled", () => { + // First ensure draft mode is enabled. + draftMode().enable() + expect(draftMode().isEnabled).toBe(true) + + disableDraftMode() + expect(draftMode().disable).toHaveBeenCalledTimes(1) + expect(draftMode().isEnabled).toBe(false) + }) + + test("returns a response object", async () => { + const response = disableDraftMode() + + expect(response).toBeInstanceOf(Response) + expect(response.ok).toBe(true) + expect(await response.text()).toBe("Draft mode is disabled") + }) +}) + +describe("getDraftData()", () => { + const draftData = { + slug: "/example", + resourceVersion: "id:1", + } + const draftDataCookie: ResponseCookie = { + name: DRAFT_DATA_COOKIE_NAME, + value: JSON.stringify(draftData), + sameSite: "none", + secure: true, + } + + test("returns empty object if draft mode disabled", () => { + cookies().set(draftDataCookie) + + const data = getDraftData() + expect(draftMode().isEnabled).toBe(false) + expect(cookies().has).toHaveBeenCalledTimes(0) + expect(cookies().get).toHaveBeenCalledTimes(0) + expect(data).toMatchObject({}) + }) + + test("returns empty object if no draft data cookie", () => { + draftMode().enable() + draftMode.mockClear() + + const data = getDraftData() + expect(draftMode).toHaveBeenCalledTimes(1) + expect(draftMode().isEnabled).toBe(true) + expect(cookies().has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME) + expect(cookies().has).toHaveBeenCalledTimes(1) + expect(cookies().get).toHaveBeenCalledTimes(0) + expect(data).toMatchObject({}) + }) + + test("returns empty object if no draft data cookie value", () => { + cookies().set({ + ...draftDataCookie, + value: "", + }) + draftMode().enable() + draftMode.mockClear() + + const data = getDraftData() + expect(draftMode).toHaveBeenCalledTimes(1) + expect(draftMode().isEnabled).toBe(true) + expect(cookies().has).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME) + expect(cookies().has).toHaveBeenCalledTimes(1) + expect(cookies().get).toHaveBeenCalledWith(DRAFT_DATA_COOKIE_NAME) + expect(cookies().get).toHaveBeenCalledTimes(1) + expect(data).toMatchObject({}) + }) + + test("returns the JSON.parse()d data", () => { + cookies().set(draftDataCookie) + draftMode().enable() + + expect(getDraftData()).toMatchObject(draftData) + }) +}) diff --git a/packages/next-drupal/tests/utils/index.ts b/packages/next-drupal/tests/utils/index.ts new file mode 100644 index 00000000..a6dd3e49 --- /dev/null +++ b/packages/next-drupal/tests/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./mocks" +export * from "./rpc" diff --git a/packages/next-drupal/tests/utils/mocks/data.ts b/packages/next-drupal/tests/utils/mocks/data.ts new file mode 100644 index 00000000..95ec1895 --- /dev/null +++ b/packages/next-drupal/tests/utils/mocks/data.ts @@ -0,0 +1,365 @@ +import type { + DrupalClientAuth, + DrupalClientAuthAccessToken, + DrupalClientAuthClientIdSecret, + DrupalClientAuthUsernamePassword, +} from "../../../src" + +// Run all tests against this env until we configure CI to setup a Drupal instance. +// TODO: Bootstrap and expose the /drupal env for testing. +export const BASE_URL = process.env["DRUPAL_BASE_URL"] as string + +const auth = { + basicAuth: { + username: "admin", + password: "example", + } as DrupalClientAuthUsernamePassword, + accessToken: { + access_token: "ECYM594IlARGc3S8KgBHvTpki0rDtWx6", + token_type: "bearer", + expires_in: 3600, + } as DrupalClientAuthAccessToken, + clientIdSecret: { + clientId: "7795065e-8ad0-45eb-a64d-73d9f3a5e943", + clientSecret: "d92Fm^ds", + } as DrupalClientAuthClientIdSecret, + function: function authFunction() { + return "custom Authentication header from authFunction" + } as DrupalClientAuth, + customAuthenticationHeader: + "custom Authentication header from string" as DrupalClientAuth, +} + +const resources = { + file: { + jsonapi: { + version: "1.0", + meta: { links: { self: { href: "http://jsonapi.org/format/1.0/" } } }, + }, + data: { + type: "file--file", + id: "641fc6a4-276d-43e9-abbd-1e51bc28ddf9", + links: { + self: { + href: "https://example.com/en/jsonapi/file/file/641fc6a4-276d-43e9-abbd-1e51bc28ddf9", + }, + }, + attributes: { + drupal_internal__fid: 1, + langcode: "en", + filename: "mediterranean-quiche-umami.jpg", + uri: { + value: "public://mediterranean-quiche-umami.jpg", + url: "/sites/default/files/mediterranean-quiche-umami.jpg", + }, + filemime: "image/jpeg", + filesize: 70160, + status: true, + created: "2022-03-21T10:52:42+00:00", + changed: "2022-03-21T10:52:42+00:00", + }, + relationships: { + uid: { + data: null, + links: { + related: { + href: "https://example.com/en/jsonapi/file/file/641fc6a4-276d-43e9-abbd-1e51bc28ddf9/uid", + }, + self: { + href: "https://example.com/en/jsonapi/file/file/641fc6a4-276d-43e9-abbd-1e51bc28ddf9/relationships/uid", + }, + }, + }, + }, + }, + links: { + self: { + href: "https://example.com/en/jsonapi/file/file/641fc6a4-276d-43e9-abbd-1e51bc28ddf9", + }, + }, + }, + mediaImage: { + jsonapi: { + version: "1.0", + meta: { links: { self: { href: "http://jsonapi.org/format/1.0/" } } }, + }, + data: { + type: "media--image", + id: "bbfe9d97-2da2-432b-a22c-0396c08e06ca", + links: { + self: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca?resourceVersion=id%3A1", + }, + }, + attributes: { + drupal_internal__mid: 1, + drupal_internal__vid: 1, + langcode: "en", + revision_created: "2022-03-21T10:52:42+00:00", + revision_log_message: null, + status: true, + name: "Deep mediterranean quiche", + created: "2022-03-21T10:52:42+00:00", + changed: "2022-03-21T10:52:42+00:00", + default_langcode: true, + revision_translation_affected: true, + path: { alias: null, pid: null, langcode: "en" }, + content_translation_source: "und", + content_translation_outdated: false, + }, + relationships: { + bundle: { + data: { + type: "media_type--media_type", + id: "afec21c2-d0a9-4e0e-8c3a-1cd6d5a8fc92", + meta: { drupal_internal__target_id: "image" }, + }, + links: { + related: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/bundle?resourceVersion=id%3A1", + }, + self: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/relationships/bundle?resourceVersion=id%3A1", + }, + }, + }, + revision_user: { + data: null, + links: { + related: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/revision_user?resourceVersion=id%3A1", + }, + self: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/relationships/revision_user?resourceVersion=id%3A1", + }, + }, + }, + uid: { + data: { + type: "user--user", + id: "256a133b-0bd7-4426-a823-b8ce81e0d778", + meta: { drupal_internal__target_id: 0 }, + }, + links: { + related: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/uid?resourceVersion=id%3A1", + }, + self: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/relationships/uid?resourceVersion=id%3A1", + }, + }, + }, + thumbnail: { + data: { + type: "file--file", + id: "641fc6a4-276d-43e9-abbd-1e51bc28ddf9", + meta: { + alt: "A delicious deep layered Mediterranean quiche with basil garnish", + title: null, + width: 768, + height: 511, + drupal_internal__target_id: 1, + }, + }, + links: { + related: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/thumbnail?resourceVersion=id%3A1", + }, + self: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/relationships/thumbnail?resourceVersion=id%3A1", + }, + }, + }, + field_media_image: { + data: { + type: "file--file", + id: "641fc6a4-276d-43e9-abbd-1e51bc28ddf9", + meta: { + alt: "A delicious deep layered Mediterranean quiche with basil garnish", + title: null, + width: 768, + height: 511, + drupal_internal__target_id: 1, + }, + }, + links: { + related: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/field_media_image?resourceVersion=id%3A1", + }, + self: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca/relationships/field_media_image?resourceVersion=id%3A1", + }, + }, + }, + }, + }, + included: [ + { + type: "file--file", + id: "641fc6a4-276d-43e9-abbd-1e51bc28ddf9", + links: { + self: { + href: "https://example.com/en/jsonapi/file/file/641fc6a4-276d-43e9-abbd-1e51bc28ddf9", + }, + }, + attributes: { + drupal_internal__fid: 1, + langcode: "en", + filename: "mediterranean-quiche-umami.jpg", + uri: { + value: "public://mediterranean-quiche-umami.jpg", + url: "/sites/default/files/mediterranean-quiche-umami.jpg", + }, + filemime: "image/jpeg", + filesize: 70160, + status: true, + created: "2022-03-21T10:52:42+00:00", + changed: "2022-03-21T10:52:42+00:00", + }, + relationships: { + uid: { + data: null, + links: { + related: { + href: "https://example.com/en/jsonapi/file/file/641fc6a4-276d-43e9-abbd-1e51bc28ddf9/uid", + }, + self: { + href: "https://example.com/en/jsonapi/file/file/641fc6a4-276d-43e9-abbd-1e51bc28ddf9/relationships/uid", + }, + }, + }, + }, + }, + ], + links: { + self: { + href: "https://example.com/en/jsonapi/media/image/bbfe9d97-2da2-432b-a22c-0396c08e06ca?include=field_media_image\u0026resourceVersion=id%3A1", + }, + }, + }, +} + +const menus = { + menuItems: { + jsonapi: { + version: "1.0", + meta: { + links: { + self: { + href: "http://jsonapi.org/format/1.0/", + }, + }, + }, + }, + data: [ + { + type: "menu_link_content--menu_link_content", + id: "standard.front_page", + attributes: { + description: "", + enabled: true, + expanded: false, + menu_name: "main", + meta: [], + options: [], + parent: "", + provider: "demo_umami", + route: { + name: "", + parameters: [], + }, + title: "Home", + url: "/en", + weight: "0", + }, + }, + { + type: "menu_link_content--menu_link_content", + id: "views_view:views.featured_articles.page_1", + attributes: { + description: "", + enabled: true, + expanded: false, + menu_name: "main", + meta: { + view_id: "featured_articles", + display_id: "page_1", + }, + options: [], + parent: "", + provider: "views", + route: { + name: "view.featured_articles.page_1", + parameters: [], + }, + title: "Articles", + url: "/en/articles", + weight: "20", + }, + }, + { + type: "menu_link_content--menu_link_content", + id: "views_view:views.recipes.page_1", + attributes: { + description: "", + enabled: true, + expanded: false, + menu_name: "main", + meta: { + view_id: "recipes", + display_id: "page_1", + }, + options: [], + parent: "", + provider: "views", + route: { + name: "view.recipes.page_1", + parameters: [], + }, + title: "Recipes", + url: "/en/recipes", + weight: "30", + }, + }, + ], + links: { + self: { + href: "https://next-drupal-test.ddev.site/jsonapi/menu_items/main", + }, + }, + }, + invalidMenu: { + jsonapi: { + version: "1.0", + meta: { + links: { + self: { + href: "http://jsonapi.org/format/1.0/", + }, + }, + }, + }, + errors: [ + { + title: "Not Found", + status: "404", + detail: + 'The "menu" parameter was not converted for the path "/jsonapi/menu_items/{menu}" (route name: "jsonapi_menu_items.menu")', + links: { + via: { + href: "https://next-drupal-test.ddev.site/jsonapi/menu_items/INVALID", + }, + info: { + href: "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5", + }, + }, + }, + ], + }, +} + +export const mocks = { + auth, + resources, + menus, +} diff --git a/packages/next-drupal/tests/utils/mocks/fetch.ts b/packages/next-drupal/tests/utils/mocks/fetch.ts new file mode 100644 index 00000000..b33122ca --- /dev/null +++ b/packages/next-drupal/tests/utils/mocks/fetch.ts @@ -0,0 +1,63 @@ +import { jest } from "@jest/globals" + +interface SpyOnFetchParams { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseBody?: any + throwErrorMessage?: string + status?: number + headers?: Record +} + +export function spyOnFetch({ + responseBody = null, + throwErrorMessage = null, + status = 200, + headers = {}, +}: SpyOnFetchParams = {}) { + return jest.spyOn(global, "fetch").mockImplementation( + fetchMockImplementation({ + responseBody, + throwErrorMessage, + status, + headers, + }) + ) +} + +export function spyOnFetchOnce({ + responseBody = null, + throwErrorMessage = null, + status = 200, + headers = {}, +}: SpyOnFetchParams) { + return jest.spyOn(global, "fetch").mockImplementationOnce( + fetchMockImplementation({ + responseBody, + throwErrorMessage, + status, + headers, + }) + ) +} + +function fetchMockImplementation({ + responseBody = null, + throwErrorMessage = null, + status = 200, + headers = {}, +}: SpyOnFetchParams) { + if (throwErrorMessage) { + return async () => { + throw new Error(throwErrorMessage) + } + } + + return async () => + new Response(JSON.stringify(responseBody || {}), { + status, + headers: { + "content-type": "application/vnd.api+json", + ...headers, + }, + }) +} diff --git a/packages/next-drupal/tests/utils/mocks/index.ts b/packages/next-drupal/tests/utils/mocks/index.ts new file mode 100644 index 00000000..96ddd33a --- /dev/null +++ b/packages/next-drupal/tests/utils/mocks/index.ts @@ -0,0 +1,3 @@ +export * from "./data" +export * from "./fetch" +export * from "./logger" diff --git a/packages/next-drupal/tests/utils/mocks/logger.ts b/packages/next-drupal/tests/utils/mocks/logger.ts new file mode 100644 index 00000000..bd696ee5 --- /dev/null +++ b/packages/next-drupal/tests/utils/mocks/logger.ts @@ -0,0 +1,10 @@ +import { Logger } from "../../../src" + +export function mockLogger(): Logger { + return { + log: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + } +} diff --git a/packages/next-drupal/tests/utils.ts b/packages/next-drupal/tests/utils/rpc.ts similarity index 76% rename from packages/next-drupal/tests/utils.ts rename to packages/next-drupal/tests/utils/rpc.ts index b7fe704f..1b4aae67 100644 --- a/packages/next-drupal/tests/utils.ts +++ b/packages/next-drupal/tests/utils/rpc.ts @@ -1,8 +1,5 @@ -import { DrupalClient } from "../src" - -// Run all tests against this env until we configure CI to setup a Drupal instance. -// TODO: Bootstrap and expose the /drupal env for testing. -export const BASE_URL = process.env["DRUPAL_BASE_URL"] as string +import { DrupalClient } from "../../src" +import { BASE_URL } from "./index" const client = new DrupalClient(BASE_URL, { auth: {