From e3fb63df120e49d27e7d5b16ddb6259be9affe59 Mon Sep 17 00:00:00 2001 From: lihbr Date: Tue, 3 Sep 2024 18:17:13 +0200 Subject: [PATCH] test: `WriteClient` core methods --- src/BaseClient.ts | 23 +- src/WriteClient.ts | 2 +- src/lib/pLimit.ts | 11 +- test/__testutils__/createRepositoryName.ts | 5 +- test/__testutils__/createWriteClient.ts | 30 +++ test/__testutils__/mockPrismicAssetAPI.ts | 95 ++++--- test/__testutils__/mockPrismicMigrationAPI.ts | 39 ++- test/writeClient-createAsset.test.ts | 224 ++++++++++++++++ test/writeClient-createAssetTag.test.ts | 100 ++++++++ test/writeClient-createDocument.test.ts | 102 ++++++++ test/writeClient-getAssets.test.ts | 241 ++++++++++++++++++ test/writeClient-migrate.test.ts | 20 +- test/writeClient-updateDocument.test.ts | 104 ++++++++ test/writeClient.test.ts | 2 +- 14 files changed, 904 insertions(+), 94 deletions(-) create mode 100644 test/__testutils__/createWriteClient.ts create mode 100644 test/writeClient-createAsset.test.ts create mode 100644 test/writeClient-createAssetTag.test.ts create mode 100644 test/writeClient-createDocument.test.ts create mode 100644 test/writeClient-getAssets.test.ts create mode 100644 test/writeClient-updateDocument.test.ts diff --git a/src/BaseClient.ts b/src/BaseClient.ts index bc3ab0fb..329d0e63 100644 --- a/src/BaseClient.ts +++ b/src/BaseClient.ts @@ -2,6 +2,13 @@ import { type LimitFunction, pLimit } from "./lib/pLimit" import { PrismicError } from "./errors/PrismicError" +/** + * The default delay used with APIs not providing rate limit headers. + * + * @internal + */ +export const UNKNOWN_RATE_LIMIT_DELAY = 1500 + /** * A universal API to make network requests. A subset of the `fetch()` API. * @@ -241,25 +248,13 @@ export class BaseClient { if (!this.queuedFetchJobs[hostname]) { this.queuedFetchJobs[hostname] = pLimit({ - limit: 1, - interval: 1500, + interval: UNKNOWN_RATE_LIMIT_DELAY, }) } - const job = this.queuedFetchJobs[hostname](() => + return this.queuedFetchJobs[hostname](() => this.createFetchJob(url, requestInit), ) - - job.finally(() => { - if ( - this.queuedFetchJobs[hostname] && - this.queuedFetchJobs[hostname].queueSize === 0 - ) { - delete this.queuedFetchJobs[hostname] - } - }) - - return job } private dedupeFetch( diff --git a/src/WriteClient.ts b/src/WriteClient.ts index 0024bc26..a0cc41a3 100644 --- a/src/WriteClient.ts +++ b/src/WriteClient.ts @@ -816,7 +816,7 @@ export class WriteClient< /** * {@link resolveAssetTagIDs} rate limiter. */ - private _resolveAssetTagIDsLimit = pLimit({ limit: 1 }) + private _resolveAssetTagIDsLimit = pLimit() /** * Resolves asset tag IDs from tag names or IDs. diff --git a/src/lib/pLimit.ts b/src/lib/pLimit.ts index a5365e51..0f929623 100644 --- a/src/lib/pLimit.ts +++ b/src/lib/pLimit.ts @@ -32,20 +32,15 @@ export type LimitFunction = { } export const pLimit = ({ - limit, interval, -}: { - limit: number - interval?: number -}): LimitFunction => { +}: { interval?: number } = {}): LimitFunction => { const queue: AnyFunction[] = [] let activeCount = 0 let lastCompletion = 0 const resumeNext = () => { - if (activeCount < limit && queue.length > 0) { + if (activeCount === 0 && queue.length > 0) { queue.shift()?.() - // Since `pendingCount` has been decreased by one, increase `activeCount` by one. activeCount++ } } @@ -96,7 +91,7 @@ export const pLimit = ({ // needs to happen asynchronously as well to get an up-to-date value for `activeCount`. await Promise.resolve() - if (activeCount < limit) { + if (activeCount === 0) { resumeNext() } })() diff --git a/test/__testutils__/createRepositoryName.ts b/test/__testutils__/createRepositoryName.ts index b049e632..1eb8880e 100644 --- a/test/__testutils__/createRepositoryName.ts +++ b/test/__testutils__/createRepositoryName.ts @@ -1,9 +1,10 @@ +import type { TaskContext } from "vitest" import { expect } from "vitest" import * as crypto from "node:crypto" -export const createRepositoryName = (): string => { - const seed = expect.getState().currentTestName +export const createRepositoryName = (ctx?: TaskContext): string => { + const seed = ctx?.task.name || expect.getState().currentTestName if (!seed) { throw new Error( `createRepositoryName() can only be called within a Vitest test.`, diff --git a/test/__testutils__/createWriteClient.ts b/test/__testutils__/createWriteClient.ts new file mode 100644 index 00000000..962d8ceb --- /dev/null +++ b/test/__testutils__/createWriteClient.ts @@ -0,0 +1,30 @@ +import type { TaskContext } from "vitest" + +import fetch from "node-fetch" + +import { createRepositoryName } from "./createRepositoryName" + +import * as prismic from "../../src" + +type CreateTestWriteClientArgs = { + repositoryName?: string +} & { + ctx: TaskContext + clientConfig?: Partial +} + +export const createTestWriteClient = ( + args: CreateTestWriteClientArgs, +): prismic.WriteClient => { + const repositoryName = args.repositoryName || createRepositoryName(args.ctx) + + return prismic.createWriteClient(repositoryName, { + fetch, + writeToken: "xxx", + migrationAPIKey: "yyy", + // We create unique endpoints so we can run tests concurrently + assetAPIEndpoint: `https://${repositoryName}.asset-api.prismic.io`, + migrationAPIEndpoint: `https://${repositoryName}.migration.prismic.io`, + ...args.clientConfig, + }) +} diff --git a/test/__testutils__/mockPrismicAssetAPI.ts b/test/__testutils__/mockPrismicAssetAPI.ts index b4e86c70..46b23e5b 100644 --- a/test/__testutils__/mockPrismicAssetAPI.ts +++ b/test/__testutils__/mockPrismicAssetAPI.ts @@ -1,12 +1,12 @@ -import type { TestContext } from "vitest" +import { type TestContext } from "vitest" import { rest } from "msw" -import { createRepositoryName } from "./createRepositoryName" - +import type { WriteClient } from "../../src" import type { Asset, GetAssetsResult, + PatchAssetParams, PostAssetResult, } from "../../src/types/api/asset/asset" import type { @@ -16,17 +16,20 @@ import type { PostAssetTagResult, } from "../../src/types/api/asset/tag" -type MockPrismicMigrationAPIV2Args = { +type MockPrismicAssetAPIArgs = { ctx: TestContext - writeToken: string + client: WriteClient + writeToken?: string expectedAsset?: Asset expectedAssets?: Asset[] expectedTag?: AssetTag expectedTags?: AssetTag[] + expectedCursor?: string + getRequiredParams?: Record } const DEFAULT_TAG: AssetTag = { - id: "091daea1-4954-46c9-a819-faabb69464ea", + id: "88888888-4444-4444-4444-121212121212", name: "fooTag", uploader_id: "uploaded_id", created_at: 0, @@ -54,19 +57,30 @@ const DEFAULT_ASSET: Asset = { tags: [], } -export const mockPrismicRestAPIV2 = ( - args: MockPrismicMigrationAPIV2Args, -): void => { - const repositoryName = createRepositoryName() - const assetAPIEndpoint = `https://asset-api.prismic.io` +export const mockPrismicAssetAPI = (args: MockPrismicAssetAPIArgs): void => { + const repositoryName = args.client.repositoryName + const assetAPIEndpoint = args.client.assetAPIEndpoint + const writeToken = args.writeToken || args.client.writeToken args.ctx.server.use( - rest.get(`${assetAPIEndpoint}/assets`, async (req, res, ctx) => { + rest.get(`${assetAPIEndpoint}assets`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) + } + + if (args.getRequiredParams) { + for (const paramKey in args.getRequiredParams) { + const requiredValue = args.getRequiredParams[paramKey] + + args.ctx + .expect(req.url.searchParams.getAll(paramKey)) + .toStrictEqual( + Array.isArray(requiredValue) ? requiredValue : [requiredValue], + ) + } } const items: Asset[] = args.expectedAssets || [DEFAULT_ASSET] @@ -75,51 +89,49 @@ export const mockPrismicRestAPIV2 = ( total: items.length, items, is_opensearch_result: false, - cursor: undefined, + cursor: args.expectedCursor, missing_ids: [], } return res(ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.post(`${assetAPIEndpoint}/assets`, async (req, res, ctx) => { + rest.post(`${assetAPIEndpoint}assets`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } const response: PostAssetResult = args.expectedAsset || DEFAULT_ASSET return res(ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.patch(`${assetAPIEndpoint}/assets/:id`, async (req, res, ctx) => { + rest.patch(`${assetAPIEndpoint}assets/:id`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) + } + const { tags, ...body } = await req.json() + + const response: PostAssetResult = { + ...(args.expectedAsset || DEFAULT_ASSET), + ...body, + tags: tags?.length + ? tags.map((id) => ({ ...DEFAULT_TAG, id })) + : (args.expectedAsset || DEFAULT_ASSET).tags, } - - const response: PostAssetResult = args.expectedAsset || DEFAULT_ASSET return res(ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.get(`${assetAPIEndpoint}/tags`, async (req, res, ctx) => { + rest.get(`${assetAPIEndpoint}tags`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } const items: AssetTag[] = args.expectedTags || [DEFAULT_TAG] @@ -128,24 +140,23 @@ export const mockPrismicRestAPIV2 = ( return res(ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.post(`${assetAPIEndpoint}/tags`, async (req, res, ctx) => { + rest.post(`${assetAPIEndpoint}tags`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || + req.headers.get("authorization") !== `Bearer ${writeToken}` || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(401), ctx.json({ error: "unauthorized" })) } const body = await req.json() + const response: PostAssetTagResult = args.expectedTag || { ...DEFAULT_TAG, + id: `${`${Date.now()}`.slice(-8)}-4954-46c9-a819-faabb69464ea`, name: body.name, } - return res(ctx.json(201), ctx.json(response)) + return res(ctx.status(201), ctx.json(response)) }), ) } diff --git a/test/__testutils__/mockPrismicMigrationAPI.ts b/test/__testutils__/mockPrismicMigrationAPI.ts index a619ad15..6a31d997 100644 --- a/test/__testutils__/mockPrismicMigrationAPI.ts +++ b/test/__testutils__/mockPrismicMigrationAPI.ts @@ -2,8 +2,7 @@ import type { TestContext } from "vitest" import { rest } from "msw" -import { createRepositoryName } from "./createRepositoryName" - +import type { WriteClient } from "../../src" import type { PostDocumentParams, PostDocumentResult, @@ -11,27 +10,30 @@ import type { PutDocumentResult, } from "../../src/types/api/migration/document" -type MockPrismicMigrationAPIV2Args = { +type MockPrismicMigrationAPIArgs = { ctx: TestContext - writeToken: string - migrationAPIKey: string + client: WriteClient + writeToken?: string + migrationAPIKey?: string expectedID?: string } -export const mockPrismicRestAPIV2 = ( - args: MockPrismicMigrationAPIV2Args, +export const mockPrismicMigrationAPI = ( + args: MockPrismicMigrationAPIArgs, ): void => { - const repositoryName = createRepositoryName() - const migrationAPIEndpoint = `https://migration.prismic.io` + const repositoryName = args.client.repositoryName + const migrationAPIEndpoint = args.client.migrationAPIEndpoint + const writeToken = args.writeToken || args.client.writeToken + const migrationAPIKey = args.migrationAPIKey || args.client.migrationAPIKey args.ctx.server.use( - rest.post(`${migrationAPIEndpoint}/documents`, async (req, res, ctx) => { + rest.post(`${migrationAPIEndpoint}documents`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || - req.headers.get("x-apy-key") !== args.migrationAPIKey || + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("x-api-key") !== migrationAPIKey || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(403), ctx.json({ Message: "forbidden" })) } const id = args.expectedID || args.ctx.mock.value.document().id @@ -47,16 +49,13 @@ export const mockPrismicRestAPIV2 = ( return res(ctx.status(201), ctx.json(response)) }), - ) - - args.ctx.server.use( - rest.put(`${migrationAPIEndpoint}/documents/:id`, async (req, res, ctx) => { + rest.put(`${migrationAPIEndpoint}documents/:id`, async (req, res, ctx) => { if ( - req.headers.get("authorization") !== `Bearer ${args.writeToken}` || - req.headers.get("x-apy-key") !== args.migrationAPIKey || + req.headers.get("authorization") !== `Bearer ${writeToken}` || + req.headers.get("x-api-key") !== migrationAPIKey || req.headers.get("repository") !== repositoryName ) { - return res(ctx.status(401)) + return res(ctx.status(403), ctx.json({ Message: "forbidden" })) } if (req.params.id !== args.expectedID) { diff --git a/test/writeClient-createAsset.test.ts b/test/writeClient-createAsset.test.ts new file mode 100644 index 00000000..3fa8d41a --- /dev/null +++ b/test/writeClient-createAsset.test.ts @@ -0,0 +1,224 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" + +import { ForbiddenError } from "../src" +import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" +import type { AssetTag } from "../src/types/api/asset/tag" + +it.concurrent("creates an asset from string content", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg") + + expect(asset.id).toBeTypeOf("string") +}) + +it.concurrent("creates an asset from file content", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset( + new File(["file"], "foo.jpg"), + "foo.jpg", + ) + + expect(asset.id).toBeTypeOf("string") +}) + +it.concurrent("creates an asset with metadata", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg", { + alt: "foo", + credits: "bar", + notes: "baz", + }) + + expect(asset.id).toBeTypeOf("string") +}) + +it.concurrent("creates an asset with an existing tag ID", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const tag: AssetTag = { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + } + + mockPrismicAssetAPI({ ctx, client, expectedTags: [tag] }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg", { + tags: [tag.id], + }) + + expect(asset.tags?.[0].id).toEqual(tag.id) +}) + +it.concurrent("creates an asset with an existing tag name", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const tag: AssetTag = { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + } + + mockPrismicAssetAPI({ ctx, client, expectedTags: [tag] }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg", { + tags: [tag.name], + }) + + expect(asset.tags?.[0].id).toEqual(tag.id) +}) + +it.concurrent("creates an asset with a new tag name", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const tag: AssetTag = { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + } + + mockPrismicAssetAPI({ ctx, client, expectedTag: tag }) + + // @ts-expect-error - testing purposes + const asset = await client.createAsset("file", "foo.jpg", { + tags: ["foo"], + }) + + expect(asset.tags?.[0].id).toEqual(tag.id) +}) + +it.concurrent("throws if asset has invalid metadata", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const filename = "foo.jpg" + const file = "https://example.com/foo.jpg" + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { notes: "0".repeat(501) }), + "notes", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: \`notes\` must be at most 500 characters]`, + ) + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { credits: "0".repeat(501) }), + "credits", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: \`credits\` must be at most 500 characters]`, + ) + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { alt: "0".repeat(501) }), + "alt", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: \`alt\` must be at most 500 characters]`, + ) + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { tags: ["0"] }), + "tags", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, + ) + + await expect( + () => + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { tags: ["0".repeat(21)] }), + "tags", + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Errors validating asset metadata: all \`tags\`'s tag must be at least 3 characters long and 20 characters at most]`, + ) + + await expect( + // @ts-expect-error - testing purposes + client.createAsset(file, filename, { + tags: [ + // Tag name + "012", + // Tag ID + "00000000-4444-4444-4444-121212121212", + ], + }), + "tags", + ).resolves.not.toBeUndefined() +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAsset("file", "foo.jpg"), + ).rejects.toThrow(ForbiddenError) +}) + +// It seems like p-limit and node-fetch are not happy friends :/ +// https://github.com/sindresorhus/p-limit/issues/64 +it.skip("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const controller = new AbortController() + controller.abort() + + mockPrismicAssetAPI({ ctx, client }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAsset("file", "foo.jpg", { + fetchOptions: { signal: controller.signal }, + }), + ).rejects.toThrow(/aborted/i) +}) + +it.concurrent("respects unknown rate limit", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const args = ["file", "foo.jpg"] + + const start = Date.now() + + // @ts-expect-error - testing purposes + await client.createAsset(...args) + + expect(Date.now() - start).toBeLessThan(UNKNOWN_RATE_LIMIT_DELAY) + + // @ts-expect-error - testing purposes + await client.createAsset(...args) + + expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) +}) diff --git a/test/writeClient-createAssetTag.test.ts b/test/writeClient-createAssetTag.test.ts new file mode 100644 index 00000000..7c1d6d7e --- /dev/null +++ b/test/writeClient-createAssetTag.test.ts @@ -0,0 +1,100 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" + +import { ForbiddenError } from "../src" +import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" +import type { AssetTag } from "../src/types/api/asset/tag" + +it.concurrent("creates a tag", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedTag: AssetTag = { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + } + + mockPrismicAssetAPI({ ctx, client, expectedTag }) + + // @ts-expect-error - testing purposes + const tag = await client.createAssetTag(expectedTag.name) + + expect(tag).toStrictEqual(expectedTag) +}) + +it.concurrent("throws if tag is invalid", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAssetTag("0"), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Asset tag name must be at least 3 characters long and 20 characters at most]`, + ) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAssetTag("0".repeat(21)), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Asset tag name must be at least 3 characters long and 20 characters at most]`, + ) + + await expect( + // @ts-expect-error - testing purposes + client.createAssetTag("valid"), + ).resolves.not.toBeUndefined() +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAssetTag("foo"), + ).rejects.toThrow(ForbiddenError) +}) + +// It seems like p-limit and node-fetch are not happy friends :/ +// https://github.com/sindresorhus/p-limit/issues/64 +it.skip("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const controller = new AbortController() + controller.abort() + + mockPrismicAssetAPI({ ctx, client }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createAssetTag("foo", { + fetchOptions: { signal: controller.signal }, + }), + ).rejects.toThrow(/aborted/i) +}) + +it.concurrent("respects unknown rate limit", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const args = ["foo"] + + const start = Date.now() + + // @ts-expect-error - testing purposes + await client.createAssetTag(...args) + + expect(Date.now() - start).toBeLessThan(UNKNOWN_RATE_LIMIT_DELAY) + + // @ts-expect-error - testing purposes + await client.createAssetTag(...args) + + expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) +}) diff --git a/test/writeClient-createDocument.test.ts b/test/writeClient-createDocument.test.ts new file mode 100644 index 00000000..82a0904c --- /dev/null +++ b/test/writeClient-createDocument.test.ts @@ -0,0 +1,102 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" + +import { ForbiddenError } from "../src" +import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" + +it.concurrent("creates a document", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + // @ts-expect-error - testing purposes + const { id } = await client.createDocument( + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + ) + + expect(id).toBe(expectedID) +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicMigrationAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createDocument( + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + ), + ).rejects.toThrow(ForbiddenError) +}) + +// It seems like p-limit and node-fetch are not happy friends :/ +// https://github.com/sindresorhus/p-limit/issues/64 +it.skip("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const controller = new AbortController() + controller.abort() + + mockPrismicMigrationAPI({ ctx, client, expectedID: "foo" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.createDocument( + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + { fetchOptions: { signal: controller.signal } }, + ), + ).rejects.toThrow(/aborted/i) +}) + +it.concurrent("respects unknown rate limit", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + const args = [ + { + type: "type", + uid: "uid", + lang: "lang", + data: {}, + }, + "Foo", + ] + + const start = Date.now() + + // @ts-expect-error - testing purposes + await client.createDocument(...args) + + expect(Date.now() - start).toBeLessThan(UNKNOWN_RATE_LIMIT_DELAY) + + // @ts-expect-error - testing purposes + await client.createDocument(...args) + + expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) +}) diff --git a/test/writeClient-getAssets.test.ts b/test/writeClient-getAssets.test.ts new file mode 100644 index 00000000..f206e706 --- /dev/null +++ b/test/writeClient-getAssets.test.ts @@ -0,0 +1,241 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" + +import { ForbiddenError } from "../src" +import { AssetType } from "../src/types/api/asset/asset" + +it.concurrent("get assets", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const { results } = await client.getAssets() + + expect(results).toBeInstanceOf(Array) +}) + +it.concurrent("supports `pageSize` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ + ctx, + client, + getRequiredParams: { + pageSize: "10", + }, + }) + + const { results } = await client.getAssets({ + pageSize: 10, + }) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `cursor` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + cursor: "foo", + } + + mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `assetType` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + assetType: AssetType.Image, + } + + mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `keyword` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + keyword: "foo", + } + + mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `ids` parameter", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + ids: ["foo", "bar"], + } + + mockPrismicAssetAPI({ ctx, client, getRequiredParams }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `tags` parameter (id)", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + tags: [ + "00000000-4444-4444-4444-121212121212", + "10000000-4444-4444-4444-121212121212", + ], + } + + mockPrismicAssetAPI({ + ctx, + client, + expectedTags: [ + { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + }, + { + id: "10000000-4444-4444-4444-121212121212", + name: "bar", + created_at: 0, + last_modified: 0, + }, + ], + getRequiredParams, + }) + + const { results } = await client.getAssets(getRequiredParams) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `tags` parameter (name)", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + tags: [ + "00000000-4444-4444-4444-121212121212", + "10000000-4444-4444-4444-121212121212", + ], + } + + mockPrismicAssetAPI({ + ctx, + client, + expectedTags: [ + { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + }, + { + id: "10000000-4444-4444-4444-121212121212", + name: "bar", + created_at: 0, + last_modified: 0, + }, + ], + getRequiredParams, + }) + + const { results } = await client.getAssets({ tags: ["foo", "bar"] }) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("supports `tags` parameter (missing)", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const getRequiredParams = { + tags: ["00000000-4444-4444-4444-121212121212"], + } + + mockPrismicAssetAPI({ + ctx, + client, + expectedTags: [ + { + id: "00000000-4444-4444-4444-121212121212", + name: "foo", + created_at: 0, + last_modified: 0, + }, + ], + getRequiredParams, + }) + + const { results } = await client.getAssets({ tags: ["foo", "bar"] }) + + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(2) +}) + +it.concurrent("returns `next` when next `cursor` is available", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client }) + + const { next: next1 } = await client.getAssets() + + ctx.expect(next1).toBeUndefined() + + const cursor = "foo" + mockPrismicAssetAPI({ ctx, client, expectedCursor: cursor }) + + const { next: next2 } = await client.getAssets() + + ctx.expect(next2).toBeInstanceOf(Function) + + mockPrismicAssetAPI({ ctx, client, getRequiredParams: { cursor } }) + + const { results } = await next2!() + ctx.expect(results).toBeInstanceOf(Array) + ctx.expect.assertions(4) +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicAssetAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => client.getAssets()).rejects.toThrow(ForbiddenError) +}) + +it.concurrent("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const controller = new AbortController() + controller.abort() + + mockPrismicAssetAPI({ ctx, client }) + + await expect(() => + client.getAssets({ + fetchOptions: { signal: controller.signal }, + }), + ).rejects.toThrow(/aborted/i) +}) diff --git a/test/writeClient-migrate.test.ts b/test/writeClient-migrate.test.ts index 164f97a5..dce47ec8 100644 --- a/test/writeClient-migrate.test.ts +++ b/test/writeClient-migrate.test.ts @@ -1,12 +1,20 @@ import { expect, it } from "vitest" +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicAssetAPI } from "./__testutils__/mockPrismicAssetAPI" +import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" +import { mockPrismicRestAPIV2 } from "./__testutils__/mockPrismicRestAPIV2" + import * as prismic from "../src" -it("createWriteClient creates a write client", () => { - const client = prismic.createWriteClient("qwerty", { - writeToken: "xxx", - migrationAPIKey: "yyy", - }) +it("migrates nothing when migration is empty", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicRestAPIV2({ ctx }) + mockPrismicAssetAPI({ ctx, client }) + mockPrismicMigrationAPI({ ctx, client }) + + const migration = prismic.createMigration() - expect(client).toBeInstanceOf(prismic.WriteClient) + await expect(client.migrate(migration)).resolves.toBeUndefined() }) diff --git a/test/writeClient-updateDocument.test.ts b/test/writeClient-updateDocument.test.ts new file mode 100644 index 00000000..0b744be0 --- /dev/null +++ b/test/writeClient-updateDocument.test.ts @@ -0,0 +1,104 @@ +import { expect, it } from "vitest" + +import { createTestWriteClient } from "./__testutils__/createWriteClient" +import { mockPrismicMigrationAPI } from "./__testutils__/mockPrismicMigrationAPI" + +import { ForbiddenError, NotFoundError } from "../src" +import { UNKNOWN_RATE_LIMIT_DELAY } from "../src/BaseClient" + +it.concurrent("updates a document", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + await expect( + // @ts-expect-error - testing purposes + client.updateDocument(expectedID, { + uid: "uid", + data: {}, + }), + ).resolves.toBeUndefined() +}) + +it.concurrent("throws not found error on not found ID", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicMigrationAPI({ ctx, client, expectedID: "not-found" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.updateDocument("foo", { + uid: "uid", + data: {}, + }), + ).rejects.toThrow(NotFoundError) +}) + +it.concurrent("respects unknown rate limit", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + const args = [ + expectedID, + { + uid: "uid", + data: {}, + }, + ] + + const start = Date.now() + + // @ts-expect-error - testing purposes + await client.updateDocument(...args) + + expect(Date.now() - start).toBeLessThan(UNKNOWN_RATE_LIMIT_DELAY) + + // @ts-expect-error - testing purposes + await client.updateDocument(...args) + + expect(Date.now() - start).toBeGreaterThanOrEqual(UNKNOWN_RATE_LIMIT_DELAY) +}) + +it.concurrent("throws forbidden error on invalid credentials", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + mockPrismicMigrationAPI({ ctx, client, writeToken: "invalid" }) + + await expect(() => + // @ts-expect-error - testing purposes + client.updateDocument("foo", { + uid: "uid", + data: {}, + }), + ).rejects.toThrow(ForbiddenError) +}) + +// It seems like p-limit and node-fetch are not happy friends :/ +// https://github.com/sindresorhus/p-limit/issues/64 +it.skip("supports abort controller", async (ctx) => { + const client = createTestWriteClient({ ctx }) + + const expectedID = "foo" + + const controller = new AbortController() + controller.abort() + + mockPrismicMigrationAPI({ ctx, client, expectedID }) + + await expect(() => + // @ts-expect-error - testing purposes + client.updateDocument( + expectedID, + { + uid: "uid", + data: {}, + }, + { fetchOptions: { signal: controller.signal } }, + ), + ).rejects.toThrow(/aborted/i) +}) diff --git a/test/writeClient.test.ts b/test/writeClient.test.ts index ef82d67f..bb8ea86b 100644 --- a/test/writeClient.test.ts +++ b/test/writeClient.test.ts @@ -2,7 +2,7 @@ import { expect, it, vi } from "vitest" import * as prismic from "../src" -it("createWriteClient creates a write client", () => { +it("`createWriteClient` creates a write client", () => { const client = prismic.createWriteClient("qwerty", { writeToken: "xxx", migrationAPIKey: "yyy",