Skip to content

Commit

Permalink
Document __support__ files (#1039)
Browse files Browse the repository at this point in the history
  • Loading branch information
scott-rc committed Dec 5, 2023
1 parent 09f02de commit e3623f9
Show file tree
Hide file tree
Showing 27 changed files with 433 additions and 276 deletions.
23 changes: 17 additions & 6 deletions spec/__support__/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,34 @@ import { config } from "../../src/services/config/config.js";
import { loadCookie } from "../../src/services/util/http.js";
import { testUser } from "./user.js";

export const testApp: App = {
/**
* A test Gadget app to use in tests.
*/
export const testApp: App = Object.freeze({
id: 1,
slug: "test",
primaryDomain: "test.gadget.app",
hasSplitEnvironments: true,
user: testUser,
};
});

export const notTestApp: App = {
/**
* Another test Gadget app to use in tests.
*
* This app does not have split environments.
*/
export const notTestApp: App = Object.freeze({
id: 2,
slug: "not-test",
primaryDomain: "not-test.gadget.app",
hasSplitEnvironments: false,
user: testUser,
};
});

export const nockTestApps = ({ optional = true } = {}): void => {
/**
* Sets up a response for the apps endpoint that `getApps` uses.
*/
export const nockTestApps = ({ optional = true, persist = true } = {}): void => {
nock(`https://${config.domains.services}`)
.get("/auth/api/apps")
.optionally(optional)
Expand All @@ -31,5 +42,5 @@ export const nockTestApps = ({ optional = true } = {}): void => {
return value === cookie;
})
.reply(200, [testApp, notTestApp])
.persist();
.persist(persist);
};
13 changes: 6 additions & 7 deletions spec/__support__/debug.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import assert from "node:assert";
import { expect } from "vitest";
import type { Fields } from "../../src/services/output/log/field.js";
import { createLogger } from "../../src/services/output/log/logger.js";

/**
* A logger just for tests.
*/
export const log = createLogger({ name: "test" });

export const logStack = (msg: Lowercase<string>, fields?: Fields): void => {
const carrier = { stack: "" };
Error.captureStackTrace(carrier, logStack);
log.error(msg, { ...fields, error: { name: "LogStack", stack: carrier.stack } });
};

/**
* @returns The current test name, describes, and filepath.
*/
export const getCurrentTest = (): { name: string; describes: string[]; filepath: string } => {
const currentTestName = expect.getState().currentTestName;
assert(currentTestName, "expected currentTestName to be defined");
Expand Down
123 changes: 72 additions & 51 deletions spec/__support__/edit-graphql.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,110 @@
import nock from "nock";
import type { Promisable } from "type-fest";
import { expect, vi } from "vitest";
import { z } from "zod";
import { ZodSchema, z } from "zod";
import type { App } from "../../src/services/app/app.js";
import type { GraphQLQuery } from "../../src/services/app/edit-graphql.js";
import { EditGraphQL } from "../../src/services/app/edit-graphql.js";
import { config } from "../../src/services/config/config.js";
import type { EditGraphQLError } from "../../src/services/error/error.js";
import { noop, unthunk } from "../../src/services/util/function.js";
import { noop, unthunk, type Thunk } from "../../src/services/util/function.js";
import { loadCookie } from "../../src/services/util/http.js";
import { isFunction } from "../../src/services/util/is.js";
import { PromiseSignal } from "../../src/services/util/promise.js";
import { testApp } from "./app.js";
import { log } from "./debug.js";

export const nockEditGraphQLResponse = <Query extends GraphQLQuery>({
query,
app = testApp,
...opts
}: {
export type NockEditGraphQLResponseOptions<Query extends GraphQLQuery> = {
/**
* The query to respond to.
*/
query: Query;
expectVariables?: Query["Variables"] | ((actual: any) => void);
response: Query["Result"] | ((body: { query: Query; variables?: Query["Variables"] }) => Promisable<Query["Result"]>);

/**
* The result to respond with. If a function is provided, it will be
* called with the variables from the request.
*/
result: Query["Result"] | ((variables: Query["Variables"]) => Promisable<Query["Result"]>);

/**
* The variables to expect in the request.
*/
expectVariables?: Thunk<Query["Variables"] | ZodSchema> | null;

/**
* The app to respond to.
* @default testApp
*/
app?: App;

/**
* Whether to keep responding to requests after the first one.
* @default false
*/
persist?: boolean;

/**
* Whether the request has to be made.
* @default true
*/
optional?: boolean;
}): PromiseSignal => {
};

/**
* Sets up a response to an {@linkcode EditGraphQL} query.
*/
export const nockEditGraphQLResponse = <Query extends GraphQLQuery>({
query,
app = testApp,
optional = false,
persist = false,
...opts
}: NockEditGraphQLResponseOptions<Query>): PromiseSignal => {
let subdomain = app.slug;
if (app.hasSplitEnvironments) {
subdomain += "--development";
}

let expectVariables: (actual: any) => void;
switch (true) {
case isFunction(opts.expectVariables):
expectVariables = opts.expectVariables;
break;
case opts.expectVariables === undefined:
expectVariables = noop;
break;
default:
expectVariables = (actual) => expect(actual).toEqual({ query, variables: opts.expectVariables });
break;
}
const expectVariables = (actual: unknown): Query["Variables"] => {
const expected = unthunk(opts.expectVariables);
if (expected instanceof ZodSchema) {
return expected.parse(actual) as Query["Variables"];
} else {
expect(actual).toEqual(expected);
return actual as Query["Variables"];
}
};

const generateResult = (variables: Query["Variables"]): Promisable<Query["Result"]> => {
if (isFunction(opts.result)) {
return opts.result(variables);
} else {
return opts.result;
}
};

const handledRequest = new PromiseSignal();
const responded = new PromiseSignal();

nock(`https://${subdomain}.${config.domains.app}`)
.post("/edit/api/graphql", (body) => body.query === query)
.matchHeader("cookie", (cookie) => loadCookie() === cookie)
.optionally(opts.optional)
.reply(200, (_uri, rawBody) => {
.optionally(optional)
.reply(200, async (_uri, rawBody) => {
try {
const body = z
.object({
query: z.literal(query),
variables: z
.record(z.unknown())
.optional()
.refine((variables) => {
expectVariables(variables);
return true;
}),
})
.parse(rawBody) as { query: Query; variables?: Query["Variables"] };

let response;
if (isFunction(opts.response)) {
response = opts.response(body);
} else {
response = opts.response;
}

handledRequest.resolve();

return response;
const body = z.object({ query: z.literal(query), variables: z.record(z.unknown()).optional() }).parse(rawBody);
const variables = expectVariables(body.variables);
const result = await generateResult(variables);
return result;
} catch (error) {
log.error("failed to generate response", { error });
handledRequest.reject(error);
throw error;
} finally {
responded.resolve();
}
})
.persist(opts.persist);
.persist(persist);

return handledRequest;
return responded;
};

export type MockSubscription<Query extends GraphQLQuery = GraphQLQuery> = {
Expand Down
3 changes: 3 additions & 0 deletions spec/__support__/env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* Sets the given environment variables for the duration of the given function.
*/
export const withEnv = <T>(env: Record<string, string | undefined>, fn: () => T): T => {
const keys = Object.keys(env);
const original = keys.map((key) => [key, process.env[key]] as const);
Expand Down
19 changes: 17 additions & 2 deletions spec/__support__/error.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { expect } from "vitest";
import { spyOnImplementing } from "vitest-mock-process";
import * as render from "../../src/services/error/report.js";
import * as report from "../../src/services/error/report.js";
import { PromiseSignal } from "../../src/services/util/promise.js";

/**
* Executes a function that is expected to throw an error and returns
* the thrown error. If the function does not throw an error, the test
* fails.
*
* @param fnThatThrows - The function that is expected to throw an error.
* @returns A Promise that resolves to the thrown error.
*/
export const expectError = async (fnThatThrows: () => unknown): Promise<any> => {
try {
await fnThatThrows();
Expand All @@ -12,10 +20,17 @@ export const expectError = async (fnThatThrows: () => unknown): Promise<any> =>
}
};

/**
* Expects {@linkcode report.reportErrorAndExit reportErrorAndExit} to
* be called with the given cause.
*
* @param expectedCause - The expected cause of the error.
* @returns A promise that resolves when the error is reported.
*/
export const expectReportErrorAndExit = async (expectedCause: unknown): Promise<void> => {
const signal = new PromiseSignal();

spyOnImplementing(render, "reportErrorAndExit", (actualCause) => {
spyOnImplementing(report, "reportErrorAndExit", (actualCause) => {
expect(actualCause).toBe(expectedCause);
signal.resolve();
return Promise.resolve() as never;
Expand Down
68 changes: 48 additions & 20 deletions spec/__support__/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ import path from "node:path";
import normalizePath from "normalize-path";
import { expect } from "vitest";

/**
* A map of file paths to file contents.
*/
export type Files = Record<string, string>;

/**
* Writes a directory with the specified files.
*
* @param dir - The directory path to write.
* @param files - An object containing file paths as keys and file contents as values.
* @returns A promise that resolves when the directory and files have been written successfully.
*/
export const writeDir = async (dir: string, files: Files): Promise<void> => {
await fs.ensureDir(dir);

Expand All @@ -18,26 +28,15 @@ export const writeDir = async (dir: string, files: Files): Promise<void> => {
}
};

// eslint-disable-next-line func-style
async function* walkDir(dir: string, root = dir): AsyncGenerator<{ absolutePath: string; stats: Stats }> {
const stats = await fs.stat(dir);
assert(stats.isDirectory(), `expected ${dir} to be a directory`);

// don't yield the root directory
if (dir !== root) {
yield { absolutePath: dir, stats };
}

for await (const entry of await fs.opendir(dir)) {
const absolutePath = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield* walkDir(absolutePath, root);
} else if (entry.isFile()) {
yield { absolutePath, stats: await fs.stat(absolutePath) };
}
}
}

/**
* Reads the contents of a directory and returns a map of file paths to
* their contents. If a directory is encountered, an empty string is
* assigned as the value for that directory path.
*
* @param dir The directory path to read.
* @returns A promise that resolves to a map of file paths to their
* contents.
*/
export const readDir = async (dir: string): Promise<Files> => {
const files = {} as Files;

Expand All @@ -55,7 +54,36 @@ export const readDir = async (dir: string): Promise<Files> => {
return files;
};

/**
* Asserts that the contents of a directory match the expected contents.
*
* @param dir - The path to the directory.
* @param expected - An object representing the expected contents of the
* directory, where the keys are file names and the values are file
* contents.
* @returns A promise that resolves when the assertion is complete.
*/
export const expectDir = async (dir: string, expected: Record<string, string>): Promise<void> => {
const actual = await readDir(dir);
expect(actual).toEqual(expected);
};

// eslint-disable-next-line func-style
async function* walkDir(dir: string, root = dir): AsyncGenerator<{ absolutePath: string; stats: Stats }> {
const stats = await fs.stat(dir);
assert(stats.isDirectory(), `expected ${dir} to be a directory`);

// don't yield the root directory
if (dir !== root) {
yield { absolutePath: dir, stats };
}

for await (const entry of await fs.opendir(dir)) {
const absolutePath = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield* walkDir(absolutePath, root);
} else if (entry.isFile()) {
yield { absolutePath, stats: await fs.stat(absolutePath) };
}
}
}
Loading

0 comments on commit e3623f9

Please sign in to comment.