Skip to content

Commit

Permalink
feat: server exports (#130) (#179)
Browse files Browse the repository at this point in the history
### Features
- [#zimic] Added the module `zimic/server` to export server resources
and allow programmatic management. The exported resources are:
  - `createInterceptorServer`
  - `InterceptorServer`
  - `InterceptorServerOptions`
  - `NotStartedInterceptorServerError`
  - `runCommand`
  - `CommandError`
  - `DEFAULT_ACCESS_CONTROL_HEADERS`
  - `DEFAULT_PREFLIGHT_STATUS_CODE`

### Refactoring
- [examples] Simplified the examples using remote interceptors. Now that
the function `runCommand` is exported from `zimic/server`, we can create
a load script to apply the mocks before starting the applications.
- [#zimic] Removed the prefix `[zimic]` from errors because it is not
necessary.

### Documentation
- [examples] Improved the documentation of some examples.

Closes #130.
  • Loading branch information
diego-aquino authored May 26, 2024
1 parent f32320f commit df29f2a
Show file tree
Hide file tree
Showing 73 changed files with 511 additions and 427 deletions.
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Zimic provides a flexible and type-safe way to mock HTTP requests.
- [`zimic browser init`](#zimic-browser-init)
- [`zimic server`](#zimic-server)
- [`zimic server start`](#zimic-server-start)
- [Programmatic usage](#programmatic-usage)
- [Changelog](#changelog)

## Getting started
Expand Down Expand Up @@ -1807,13 +1808,21 @@ Positionals:
[string]

Options:
-h, --hostname The hostname to start the server on.
--help Show help [boolean]
--version Show version number [boolean]
-h, --hostname The hostname to start the server on.
[string] [default: "localhost"]
-p, --port The port to start the server on. [number]
-e, --ephemeral Whether the server should stop automatically after the
on-ready command finishes. If no on-ready command is provided
and ephemeral is true, the server will stop immediately after
starting. [boolean] [default: false]
-p, --port The port to start the server on. [number]
-e, --ephemeral Whether the server should stop automatically
after the on-ready command finishes. If no
on-ready command is provided and ephemeral is
true, the server will stop immediately after
starting. [boolean] [default: false]
-l, --log-unhandled-requests Whether to log a warning when no interceptors
were found for the base URL of a request. If an
interceptor was matched, the logging behavior
for that base URL is configured in the
interceptor itself. [boolean]
```
You can use this command to start an independent server:
Expand All @@ -1831,6 +1840,28 @@ zimic server start --port 4000 --ephemeral -- npm run test
The command after `--` will be executed when the server is ready. The flag `--ephemeral` indicates that the server
should automatically stop after the command finishes.
#### Programmatic usage
The module `zimic/server` exports resources for managing interceptor servers programmatically. Even though we recommend
using the CLI, this module is a valid alternative for more advanced use cases.
```ts
import { createInterceptorServer, runCommand } from 'zimic/server';

const server = createInterceptorServer({ hostname: 'localhost', port: 3000 });
await server.start();

// Run a command when the server is ready
const [command, ...commandArguments] = process.argv.slice(3);
await runCommand(command, commandArguments);

await server.stop();
```
The helper function `runCommand` is useful to run a shell command in server scripts. The
[Next.js App Router](./examples/README.md#nextjs) and the [Playwright](./examples/README.md#playwright) examples use
this function to run the application after the interceptor server is ready and all mocks are set up.
---
## Changelog
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, expectTypeOf, it } from 'vitest';
import {
createInterceptorServer,
InterceptorServer,
InterceptorServerOptions,
NotStartedInterceptorServerError,
runCommand,
CommandError,
DEFAULT_ACCESS_CONTROL_HEADERS,
DEFAULT_PREFLIGHT_STATUS_CODE,
} from 'zimic0/server';

describe('Exports (Node.js)', () => {
it('should export all expected resources', () => {
expect(typeof createInterceptorServer).toBe('function');
expectTypeOf<InterceptorServer>().not.toBeAny();
expectTypeOf<InterceptorServerOptions>().not.toBeAny();
expectTypeOf<NotStartedInterceptorServerError>().not.toBeAny();
expect(typeof NotStartedInterceptorServerError).toBe('function');
expect(typeof runCommand).toBe('function');
expectTypeOf<CommandError>().not.toBeAny();
expect(typeof CommandError).toBe('function');

expect(DEFAULT_ACCESS_CONTROL_HEADERS).toEqual(expect.any(Object));
expect(DEFAULT_PREFLIGHT_STATUS_CODE).toEqual(expect.any(Number));
});
});
3 changes: 1 addition & 2 deletions examples/with-next-js-app/.env.development
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
NODE_ENV=development

ZIMIC_SERVER_URL=http://localhost:3005
NEXT_PUBLIC_GITHUB_API_BASE_URL=$ZIMIC_SERVER_URL/github
GITHUB_API_BASE_URL=https://api.github.com
3 changes: 3 additions & 0 deletions examples/with-next-js-app/.env.test
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
NODE_ENV=test

ZIMIC_SERVER_URL=http://localhost:3005
GITHUB_API_BASE_URL=$ZIMIC_SERVER_URL/github
26 changes: 11 additions & 15 deletions examples/with-next-js-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@
Zimic + Next.js App Router
</h2>

This example uses Zimic with [Next.js](https://nextjs.org). The application is verified with end-to-end tests using
[Playwright](https://playwright.dev).
This example uses Zimic with [Next.js](https://nextjs.org).

## Application

The application is a simple [Next.js](https://nextjs.org) application using the
[App Router](https://nextjs.org/docs/app). It fetches repositories from the
[GitHub API](https://docs.github.com/en/rest).
The application is a simple [Next.js](https://nextjs.org) project using the [App Router](https://nextjs.org/docs/app).
It fetches repositories from the [GitHub API](https://docs.github.com/en/rest).

- Application: [`src/app/app/page.page.tsx`](./src/app/page.tsx)
- Interceptor provider:
[`src/providers/interceptors/InterceptorProvider.tsx`](./src/providers/interceptors/InterceptorProvider.tsx)
- This provider is used to apply Zimic mocks when the application is started in development.
- Application: [`src/app/page.tsx`](./src/app/page.tsx)
- GitHub fetch: [`src/services/github.ts`](./src/services/github.ts)
- Before fetching resources, it is necessary to wait for the interceptors and fixtures to be loaded. This is done via
`await waitForLoadedInterceptors();`.

A `postinstall` in [`package.json`](./package.json) script is used to install Playwright's browsers.
A `postinstall` script in [`package.json`](./package.json) is used to install Playwright's browsers.

The script [`tests/interceptors/scripts/load.ts`](./tests/interceptors/scripts/load.ts) loads the interceptors and mocks
before the application is started in development. It is used by the command `dev:mock` in
[`package.json`](./package.json).

## Testing

Expand All @@ -28,8 +25,7 @@ GitHub API and simulate a test case where the repository is found and another wh

### Zimic

- GitHub interceptor: [`tests/interceptors/github/interceptor.ts`](./tests/interceptors/github/interceptor.ts)
- Fixtures: [`tests/interceptors/github/fixtures.ts`](./tests/interceptors/github/fixtures.ts)
- GitHub interceptor and mocks: [`tests/interceptors/github.ts`](./tests/interceptors/github.ts)

### Test

Expand Down Expand Up @@ -62,7 +58,7 @@ GitHub API and simulate a test case where the repository is found and another wh
1. Start the application:

```bash
pnpm run dev
pnpm run dev:mock
```

After started, the application will be available at [http://localhost:3004](http://localhost:3004).
Expand Down
5 changes: 4 additions & 1 deletion examples/with-next-js-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"version": "0.0.0",
"private": false,
"scripts": {
"dev": "dotenv -c development -- zimic server start --port 3005 --ephemeral -- next dev --turbo --port 3004",
"dev": "dotenv -c development -- next dev --turbo --port 3004",
"dev:mock": "dotenv -c test -- zimic server start --port 3005 --ephemeral -- pnpm dev:load-interceptors -- pnpm dev",
"dev:load-interceptors": "tsx ./tests/interceptors/scripts/load.ts",
"test": "dotenv -c test -- dotenv -c development -- playwright test",
"test:turbo": "pnpm run test",
"types:check": "tsc --noEmit",
Expand All @@ -25,6 +27,7 @@
"@types/react-dom": "^18.3.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.1",
"tsx": "^4.7.0",
"typescript": "^5.4.3",
"zimic": "latest"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/with-next-js-app/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default defineConfig({
],

webServer: {
command: 'pnpm run dev',
command: 'pnpm run dev:mock',
port: 3004,
stdout: 'pipe',
stderr: 'pipe',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test, { expect } from '@playwright/test';

import { githubFixtures } from '../../tests/interceptors/github/fixtures';
import { githubFixtures } from '../../tests/interceptors/github';

test.describe('Home page', () => {
const { repository } = githubFixtures;
Expand Down
9 changes: 2 additions & 7 deletions examples/with-next-js-app/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { PropsWithChildren } from 'react';

import { loadInterceptors } from '../../tests/interceptors';
import InterceptorProvider from '../providers/interceptors/InterceptorProvider';

import '../styles/global.css';

const inter = Inter({ subsets: ['latin'] });
Expand All @@ -15,13 +12,11 @@ export const metadata: Metadata = {
description: 'Generated by create-next-app',
};

async function RootLayout({ children }: PropsWithChildren) {
await loadInterceptors();

function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en">
<body className={clsx(inter.className, 'bg-slate-100 flex flex-col items-center justify-center min-h-screen')}>
<InterceptorProvider>{children}</InterceptorProvider>
{children}
</body>
</html>
);
Expand Down
2 changes: 1 addition & 1 deletion examples/with-next-js-app/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function HomePage({ searchParams }: Props) {
<GitHubRepositoryForm />

{shouldFetchRepository && (
<Suspense key={`${ownerName}-${repositoryName}`} fallback={<p role="status">Loading...</p>}>
<Suspense fallback={<p role="status">Loading...</p>}>
<GitHubRepositoryShowcase ownerName={ownerName} repositoryName={repositoryName} />
</Suspense>
)}
Expand Down
2 changes: 1 addition & 1 deletion examples/with-next-js-app/src/config/environment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const environment = {
GITHUB_API_BASE_URL: process.env.NEXT_PUBLIC_GITHUB_API_BASE_URL ?? '',
GITHUB_API_BASE_URL: process.env.GITHUB_API_BASE_URL ?? '',
};

export default environment;

This file was deleted.

7 changes: 2 additions & 5 deletions examples/with-next-js-app/src/services/github.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { JSONValue } from 'zimic';

import { waitForLoadedInterceptors } from '../../tests/interceptors';
import environment from '../config/environment';

const BASE_URL = environment.GITHUB_API_BASE_URL;
const GITHUB_API_BASE_URL = environment.GITHUB_API_BASE_URL;
const CACHE_STRATEGY = process.env.NODE_ENV === 'production' ? 'default' : 'no-store';

export type GitHubRepository = JSONValue<{
Expand All @@ -15,13 +14,11 @@ export type GitHubRepository = JSONValue<{
}>;

export async function fetchGitHubRepository(ownerName: string, repositoryName: string) {
await waitForLoadedInterceptors();

try {
const sanitizedOwnerName = encodeURIComponent(ownerName);
const sanitizedRepositoryName = encodeURIComponent(repositoryName);

const response = await fetch(`${BASE_URL}/repos/${sanitizedOwnerName}/${sanitizedRepositoryName}`, {
const response = await fetch(`${GITHUB_API_BASE_URL}/repos/${sanitizedOwnerName}/${sanitizedRepositoryName}`, {
cache: CACHE_STRATEGY,
});

Expand Down
43 changes: 43 additions & 0 deletions examples/with-next-js-app/tests/interceptors/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { http } from 'zimic/interceptor';

import environment from '../../src/config/environment';
import { GitHubRepository } from '../../src/services/github';

const githubInterceptor = http.createInterceptor<{
'/repos/:owner/:name': {
GET: {
response: {
200: { body: GitHubRepository };
404: { body: { message: string } };
500: { body: { message: string } };
};
};
};
}>({
type: 'remote',
baseURL: environment.GITHUB_API_BASE_URL,
});

export const githubFixtures = {
repository: {
id: 1,
name: 'example',
full_name: 'owner/example',
html_url: 'https://github.com/owner/example',
owner: { login: 'owner' },
} satisfies GitHubRepository,

async apply() {
await githubInterceptor.get('/repos/:owner/:name').respond({
status: 404,
body: { message: 'Not Found' },
});

await githubInterceptor.get(`/repos/${this.repository.owner.login}/${this.repository.name}`).respond({
status: 200,
body: this.repository,
});
},
};

export default githubInterceptor;
26 changes: 0 additions & 26 deletions examples/with-next-js-app/tests/interceptors/github/fixtures.ts

This file was deleted.

21 changes: 0 additions & 21 deletions examples/with-next-js-app/tests/interceptors/github/interceptor.ts

This file was deleted.

Loading

0 comments on commit df29f2a

Please sign in to comment.