Skip to content

Commit

Permalink
add standalone mock server
Browse files Browse the repository at this point in the history
  • Loading branch information
alan2207 committed Jun 2, 2024
1 parent e8181b4 commit e87c33c
Show file tree
Hide file tree
Showing 16 changed files with 394 additions and 59 deletions.
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
VITE_APP_API_URL=https:https://api.bulletproofapp.com
VITE_APP_API_URL=http:https://localhost:8080/api
VITE_APP_ENABLE_API_MOCKING=true


# If you want to run standalone mock server, you can use the following configuration:
# VITE_APP_ENABLE_API_MOCKING=false
# VITE_APP_MOCK_API_PORT=8080
# VITE_APP_URL=http:https://localhost:3000
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*


# local
mocked-db.json

26 changes: 26 additions & 0 deletions mock-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createMiddleware } from '@mswjs/http-middleware';
import cors from 'cors';
import express from 'express';
import logger from 'pino-http';

import { env } from './src/config/env';
import { handlers } from './src/testing/mocks/handlers';

const app = express();

app.use(
cors({
origin: env.APP_URL,
credentials: true,
}),
);

app.use(express.json());
app.use(logger());
app.use(createMiddleware(...handlers));

app.listen(env.APP_MOCK_API_PORT, () => {
console.log(
`Mock API server started at http:https://localhost:${env.APP_MOCK_API_PORT}`,
);
});
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
"check-types": "tsc --project tsconfig.json --pretty --noEmit",
"generate": "plop",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"build-storybook": "storybook build",
"run-mock-server": "vite-node mock-server.ts | pino-pretty -c"
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@mswjs/http-middleware": "^0.10.1",
"@ngneat/falso": "^7.2.0",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
Expand All @@ -29,12 +31,15 @@
"axios": "^1.6.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"dayjs": "^1.11.11",
"dompurify": "^3.1.1",
"eslint-plugin-check-file": "^2.8.0",
"express": "^4.19.2",
"lucide-react": "^0.378.0",
"marked": "^12.0.2",
"nanoid": "^5.0.7",
"pino-http": "^10.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
Expand Down Expand Up @@ -62,6 +67,7 @@
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^15.0.5",
"@testing-library/user-event": "^14.5.2",
"@types/cors": "^2.8.17",
"@types/dompurify": "^3.0.5",
"@types/js-cookie": "^3.0.6",
"@types/marked": "^6.0.0",
Expand Down Expand Up @@ -91,13 +97,15 @@
"jsdom": "^24.0.0",
"lint-staged": "^15.2.2",
"msw": "^2.2.14",
"pino-pretty": "^11.1.0",
"plop": "^4.0.1",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"storybook": "^8.0.9",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vite-node": "^1.6.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.5.2"
},
Expand Down
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ export default defineConfig({
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
testMatch: /.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
storageState: 'e2e/.auth/user.json',
},

dependencies: ['setup'],
},
],
Expand Down
2 changes: 2 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const createEnv = () => {
.refine((s) => s === 'true' || s === 'false')
.transform((s) => s === 'true')
.optional(),
APP_URL: z.string().optional().default('http:https://localhost:3000'),
APP_MOCK_API_PORT: z.string().optional().default('8080'),
});

const envVars = Object.entries(import.meta.env).reduce<
Expand Down
8 changes: 8 additions & 0 deletions src/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ function authRequestInterceptor(config: InternalAxiosRequestConfig) {
if (config.headers) {
config.headers.Accept = 'application/json';
}

config.withCredentials = true;
return config;
}

Expand All @@ -27,6 +29,12 @@ api.interceptors.response.use(
message,
});

if (error.response?.status === 401) {
const searchParams = new URLSearchParams();
const redirectTo = searchParams.get('redirectTo');
window.location.href = `/auth/login?redirectTo=${redirectTo}`;
}

return Promise.reject(error);
},
);
48 changes: 41 additions & 7 deletions src/testing/mocks/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,52 @@ export const db = factory(models);

export type Model = keyof typeof models;

export const loadDb = () =>
Object.assign(JSON.parse(window.localStorage.getItem('msw-db') || '{}'));
const dbFilePath = 'mocked-db.json';

export const persistDb = (model: Model) => {
export const loadDb = async () => {
// If we are running in a Node.js environment
if (typeof window === 'undefined') {
const { readFile, writeFile } = await import('fs/promises');
try {
const data = await readFile(dbFilePath, 'utf8');
return JSON.parse(data);
} catch (error: any) {
if (error?.code === 'ENOENT') {
const emptyDB = {};
await writeFile(dbFilePath, JSON.stringify(emptyDB, null, 2));
return emptyDB;
} else {
console.error('Error loading mocked DB:', error);
return null;
}
}
}
// If we are running in a browser environment
return Object.assign(
JSON.parse(window.localStorage.getItem('msw-db') || '{}'),
);
};

export const storeDb = async (key: string, data: string) => {
// If we are running in a Node.js environment
if (typeof window === 'undefined') {
const { writeFile } = await import('fs/promises');
await writeFile(dbFilePath, data);
} else {
// If we are running in a browser environment
window.localStorage.setItem('msw-db', data);
}
};

export const persistDb = async (model: Model) => {
if (process.env.NODE_ENV === 'test') return;
const data = loadDb();
const data = await loadDb();
data[model] = db[model].getAll();
window.localStorage.setItem('msw-db', JSON.stringify(data));
await storeDb('msw-db', JSON.stringify(data));
};

export const initializeDb = () => {
const database = loadDb();
export const initializeDb = async () => {
const database = await loadDb();
Object.entries(db).forEach(([key, model]) => {
const dataEntres = database[key];
if (dataEntres) {
Expand Down
6 changes: 3 additions & 3 deletions src/testing/mocks/handlers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const authHandlers = [
const team = db.team.create({
name: userObject.teamName ?? `${userObject.firstName} Team`,
});
persistDb('team');
await persistDb('team');
teamId = team.id;
role = 'ADMIN';
} else {
Expand Down Expand Up @@ -85,7 +85,7 @@ export const authHandlers = [
teamId,
});

persistDb('user');
await persistDb('user');

const result = authenticate({
email: userObject.email,
Expand Down Expand Up @@ -153,7 +153,7 @@ export const authHandlers = [
await networkDelay();

try {
const user = requireAuth(cookies, false);
const { user } = requireAuth(cookies);
return HttpResponse.json(user);
} catch (error: any) {
return HttpResponse.json(
Expand Down
19 changes: 14 additions & 5 deletions src/testing/mocks/handlers/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ export const commentsHandlers = [
await networkDelay();

try {
requireAuth(cookies);
const { error } = requireAuth(cookies);
if (error) {
return HttpResponse.json({ message: error }, { status: 401 });
}
const url = new URL(request.url);
const discussionId = url.searchParams.get('discussionId') || '';
const comments = db.comment
Expand Down Expand Up @@ -52,13 +55,16 @@ export const commentsHandlers = [
await networkDelay();

try {
const user = requireAuth(cookies);
const { user, error } = requireAuth(cookies);
if (error) {
return HttpResponse.json({ message: error }, { status: 401 });
}
const data = (await request.json()) as CreateCommentBody;
const result = db.comment.create({
authorId: user?.id,
...data,
});
persistDb('comment');
await persistDb('comment');
return HttpResponse.json(result);
} catch (error: any) {
return HttpResponse.json(
Expand All @@ -74,7 +80,10 @@ export const commentsHandlers = [
await networkDelay();

try {
const user = requireAuth(cookies);
const { user, error } = requireAuth(cookies);
if (error) {
return HttpResponse.json({ message: error }, { status: 401 });
}
const commentId = params.commentId as string;
const result = db.comment.delete({
where: {
Expand All @@ -88,7 +97,7 @@ export const commentsHandlers = [
}),
},
});
persistDb('comment');
await persistDb('comment');
return HttpResponse.json(result);
} catch (error: any) {
return HttpResponse.json(
Expand Down
31 changes: 23 additions & 8 deletions src/testing/mocks/handlers/discussions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export const discussionsHandlers = [
await networkDelay();

try {
const user = requireAuth(cookies);
const { user, error } = requireAuth(cookies);
if (error) {
return HttpResponse.json({ message: error }, { status: 401 });
}
const result = db.discussion
.findMany({
where: {
Expand Down Expand Up @@ -57,7 +60,10 @@ export const discussionsHandlers = [
await networkDelay();

try {
const user = requireAuth(cookies);
const { user, error } = requireAuth(cookies);
if (error) {
return HttpResponse.json({ message: error }, { status: 401 });
}
const discussionId = params.discussionId as string;
const discussion = db.discussion.findFirst({
where: {
Expand Down Expand Up @@ -106,15 +112,18 @@ export const discussionsHandlers = [
await networkDelay();

try {
const user = requireAuth(cookies);
const { user, error } = requireAuth(cookies);
if (error) {
return HttpResponse.json({ message: error }, { status: 401 });
}
const data = (await request.json()) as DiscussionBody;
requireAdmin(user);
const result = db.discussion.create({
teamId: user?.teamId,
authorId: user?.id,
...data,
});
persistDb('discussion');
await persistDb('discussion');
return HttpResponse.json(result);
} catch (error: any) {
return HttpResponse.json(
Expand All @@ -130,7 +139,10 @@ export const discussionsHandlers = [
await networkDelay();

try {
const user = requireAuth(cookies);
const { user, error } = requireAuth(cookies);
if (error) {
return HttpResponse.json({ message: error }, { status: 401 });
}
const data = (await request.json()) as DiscussionBody;
const discussionId = params.discussionId as string;
requireAdmin(user);
Expand All @@ -145,7 +157,7 @@ export const discussionsHandlers = [
},
data,
});
persistDb('discussion');
await persistDb('discussion');
return HttpResponse.json(result);
} catch (error: any) {
return HttpResponse.json(
Expand All @@ -162,7 +174,10 @@ export const discussionsHandlers = [
await networkDelay();

try {
const user = requireAuth(cookies);
const { user, error } = requireAuth(cookies);
if (error) {
return HttpResponse.json({ message: error }, { status: 401 });
}
const discussionId = params.discussionId as string;
requireAdmin(user);
const result = db.discussion.delete({
Expand All @@ -172,7 +187,7 @@ export const discussionsHandlers = [
},
},
});
persistDb('discussion');
await persistDb('discussion');
return HttpResponse.json(result);
} catch (error: any) {
return HttpResponse.json(
Expand Down
10 changes: 10 additions & 0 deletions src/testing/mocks/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { HttpResponse, http } from 'msw';

import { env } from '@/config/env';

import { networkDelay } from '../utils';

import { authHandlers } from './auth';
import { commentsHandlers } from './comments';
import { discussionsHandlers } from './discussions';
Expand All @@ -10,4 +16,8 @@ export const handlers = [
...discussionsHandlers,
...teamsHandlers,
...usersHandlers,
http.get(`${env.API_URL}/healthcheck`, async () => {
await networkDelay();
return HttpResponse.json({ ok: true });
}),
];

0 comments on commit e87c33c

Please sign in to comment.