Skip to content

Commit

Permalink
test: WriteClient core methods
Browse files Browse the repository at this point in the history
  • Loading branch information
lihbr committed Sep 3, 2024
1 parent 55cb476 commit e3fb63d
Show file tree
Hide file tree
Showing 14 changed files with 904 additions and 94 deletions.
23 changes: 9 additions & 14 deletions src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/WriteClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 3 additions & 8 deletions src/lib/pLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++
}
}
Expand Down Expand Up @@ -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()
}
})()
Expand Down
5 changes: 3 additions & 2 deletions test/__testutils__/createRepositoryName.ts
Original file line number Diff line number Diff line change
@@ -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.`,
Expand Down
30 changes: 30 additions & 0 deletions test/__testutils__/createWriteClient.ts
Original file line number Diff line number Diff line change
@@ -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<prismic.WriteClientConfig>
}

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,
})
}
95 changes: 53 additions & 42 deletions test/__testutils__/mockPrismicAssetAPI.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<string, string | string[]>
}

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,
Expand Down Expand Up @@ -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]
Expand All @@ -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<PatchAssetParams>()

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]
Expand All @@ -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<PostAssetTagParams>()

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))
}),
)
}
39 changes: 19 additions & 20 deletions test/__testutils__/mockPrismicMigrationAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,38 @@ import type { TestContext } from "vitest"

import { rest } from "msw"

import { createRepositoryName } from "./createRepositoryName"

import type { WriteClient } from "../../src"
import type {
PostDocumentParams,
PostDocumentResult,
PutDocumentParams,
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
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit e3fb63d

Please sign in to comment.