Skip to content

Commit

Permalink
test: add tests for kv:namespace list
Browse files Browse the repository at this point in the history
This commit also refactors mock-cfetch to be a TypeScript file, with typings.
  • Loading branch information
petebacondarwin committed Jan 2, 2022
1 parent 5856807 commit ab27cc0
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 77 deletions.
9 changes: 8 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"cSpell.words": ["cfetch", "execa", "iarna", "Miniflare", "Positionals"]
"cSpell.words": [
"cfetch",
"execa",
"iarna",
"keyvalue",
"Miniflare",
"Positionals"
]
}
53 changes: 35 additions & 18 deletions packages/wrangler/src/__tests__/kv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,31 +156,48 @@ describe("wrangler", () => {
});

describe("list", () => {
it("should list namespaces", async () => {
const KVNamespaces: { title: string; id: string }[] = [
{ title: "title-1", id: "id-1" },
{ title: "title-2", id: "id-2" },
];
function mockListRequest(namespaces: unknown[]) {
const requests = { count: 0 };
setMock(
"/accounts/:accountId/storage/kv/namespaces\\?:qs",
(uri, init) => {
expect(uri[0]).toContain(
"/accounts/some-account-id/storage/kv/namespaces"
);
expect(uri[2]).toContain("per_page=100");
expect(uri[2]).toContain("order=title");
expect(uri[2]).toContain("direction=asc");
expect(uri[2]).toContain("page=1");
([_url, accountId, query], init) => {
requests.count++;
expect(accountId).toEqual("some-account-id");
expect(query).toContain("per_page=100");
expect(query).toContain("order=title");
expect(query).toContain("direction=asc");
expect(query).toContain("page=");
expect(init).toBe(undefined);
return KVNamespaces;
const pageSize = Number(/\bper_page=(\d+)\b/.exec(query)[1]);
const page = Number(/\bpage=(\d+)/.exec(query)[1]);
return namespaces.slice((page - 1) * pageSize, page * pageSize);
}
);
return requests;
}

it("should list namespaces", async () => {
const KVNamespaces = [
{ title: "title-1", id: "id-1" },
{ title: "title-2", id: "id-2" },
];
mockListRequest(KVNamespaces);
const { stdout } = await runWrangler("kv:namespace list");
const namespaces = JSON.parse(stdout);
expect(namespaces).toEqual(KVNamespaces);
});

it("should make multiple requests for paginated results", async () => {
// Create a lot of mock namespaces, so that the cfetch requests will be paginated
const KVNamespaces = [];
for (let i = 0; i < 550; i++) {
KVNamespaces.push({ title: "title-" + i, id: "id-" + i });
}
const requests = mockListRequest(KVNamespaces);
const { stdout } = await runWrangler("kv:namespace list");
const namespaces = JSON.parse(stdout) as {
id: string;
title: string;
}[];
const namespaces = JSON.parse(stdout);
expect(namespaces).toEqual(KVNamespaces);
expect(requests.count).toEqual(6);
});
});

Expand Down
45 changes: 0 additions & 45 deletions packages/wrangler/src/__tests__/mock-cfetch.js

This file was deleted.

63 changes: 63 additions & 0 deletions packages/wrangler/src/__tests__/mock-cfetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// This file mocks ../cfetch.ts
// so we can insert whatever responses we want from it
import { pathToRegexp } from "path-to-regexp";

// Sadly we cannot give use the correct `RequestInit` type from node-fetch.
// Jest needs to transform the code as part of the module mocking, and it doesn't know how to cope with such types.
type RequestInit = { method: string; body: string };
type MockHandler = (uri: RegExpExecArray, init?: RequestInit) => unknown;
type MockFetch = {
regexp: RegExp;
method: string | undefined;
handler: MockHandler;
};
type RemoveMockFn = () => void;

let mocks: MockFetch[] = [];

export default function mockCfetch(
resource: string,
init: RequestInit
): unknown {
for (const { regexp, method, handler } of mocks) {
const uri = regexp.exec(resource);
// Do the resource path and (if specified) the HTTP method match?
if (uri !== null && (!method || method === init.method)) {
// The `resource` regular expression will extract the labelled groups from the URL.
// These are passed through to the `handler` call, to allow it to do additional checks or behaviour.
return handler(uri, init); // TODO: should we have some kind of fallthrough system? we'll see.
}
}
throw new Error(`no mocks found for ${resource}`);
}

/**
* Specify an expected resource path that is to be handled.
*/
export function setMock(
resource: string,
method: string,
handler: MockHandler
): RemoveMockFn;
export function setMock(resource: string, handler: MockHandler): RemoveMockFn;
export function setMock(resource: string, ...args: unknown[]): RemoveMockFn {
const handler = args.pop() as MockHandler;
const method = args.pop() as string;
const mock = {
resource,
method,
handler,
regexp: pathToRegexp(resource),
};
mocks.push(mock);
return () => {
mocks = mocks.filter((x) => x !== mock);
};
}

export function unsetAllMocks() {
mocks = [];
}

export const CF_API_BASE_URL =
process.env.CF_API_BASE_URL || "https://api.cloudflare.com/client/v4";
38 changes: 25 additions & 13 deletions packages/wrangler/src/kv.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,32 @@ export async function createNamespace(
return response.id;
}

export async function listNamespaces(accountId: string) {
let page = 1,
done = false,
results = [];
while (!(done || results.length % 100 !== 0)) {
const json = await cfetch<
{ id: string; title: string; supports_url_encoding: boolean }[]
>(
`/accounts/${accountId}/storage/kv/namespaces?per_page=100&order=title&direction=asc&page=${page}`
/**
* The information about a namespace that is returned from `listNamespaces()`.
*/
export interface KVNamespaceInfo {
id: string;
title: string;
supports_url_encoding?: boolean;
}

/**
* Fetch a list of all the namespaces under the given `accountId`.
*/
export async function listNamespaces(
accountId: string
): Promise<KVNamespaceInfo[]> {
const pageSize = 100;
let page = 1;
const results: KVNamespaceInfo[] = [];
while (results.length % pageSize === 0) {
const json = await cfetch<KVNamespaceInfo[]>(
`/accounts/${accountId}/storage/kv/namespaces?per_page=${pageSize}&order=title&direction=asc&page=${page}`
);
page++;
results = [...results, ...json];
if (json.length === 0) {
done = true;
results.push(...json);
if (json.length < pageSize) {
break;
}
}
return results;
Expand Down Expand Up @@ -155,7 +167,7 @@ export function getNamespaceId({
);
}

// TODO: either a bespoke arg type for this function to avoid undefineds or a EnvOrConfig type
// TODO: either a bespoke arg type for this function to avoid `undefined`s or an `EnvOrConfig` type
return getNamespaceId({
binding,
"namespace-id": namespaceId,
Expand Down

0 comments on commit ab27cc0

Please sign in to comment.