typescript-mock-server 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ have to update your mock, otherwise you will receive compile errors.
6
6
  # Quickstart
7
7
  The easiest way to check out this stub/mock server is by installing it as a (dev)dependency and then
8
8
  add a script to you scripts section: `npm run --prefix node_modules/typescript-mock-server start -- --path=$INIT_CWD/tms-models`.
9
- Your models should export a data const and your file should be named as `^(get|post){1}(-\d)?.ts$`.
9
+ Your models should export a data const (or a function receiving `req` and `res`) and your file should be named as `^(get|post){1}(-\d)?.ts$`.
10
10
  Changes are being picked up automatically, so no need for a restart. When you add files, you have to restart.
11
11
 
12
12
  Check out the [working example project](https://github.com/GuyT07/typescript-mock-server-examle) and [the source](https://github.com/GuyT07/typescript-mock-server/tree/main/tms-models/users).
@@ -38,7 +38,7 @@ You should have the following structure:
38
38
 
39
39
  Within the model file you can import/create your model:
40
40
 
41
- ```
41
+ ```typescript
42
42
  export interface User {
43
43
  id: number;
44
44
  firstName: string;
@@ -48,6 +48,7 @@ export interface User {
48
48
 
49
49
  const newDate = () => new Date();
50
50
 
51
+ // You can export a static object
51
52
  export const data: User[] = [{
52
53
  id: 1,
53
54
  firstName: 'Guy',
@@ -60,6 +61,12 @@ export const data: User[] = [{
60
61
  creationDate: newDate()
61
62
  }];
62
63
 
64
+ // OR you can export a function to access request/response context
65
+ export const data = (req: Request, res: Response): User[] => {
66
+ console.log(req.query);
67
+ return [{ id: 1, firstName: 'Dynamic', lastName: 'User', creationDate: new Date() }];
68
+ }
69
+
63
70
  export const config: RequestConfig = {
64
71
  delay: 2000, // or you can use an interval like {min: 2000, max: 5000}
65
72
  statusCode: 418
@@ -80,17 +87,20 @@ Following dependencies are being used:
80
87
  - [x] Improve paths/way to start
81
88
  - [x] Support different headers/configurations (delays, status codes, ...)
82
89
  - [x] Support most used HTTP methods
83
- - [ ] Add tests
90
+ - [x] Add tests
84
91
  - [x] Refactor, split up in separate classes (first check if people actually want to use the tool)
85
92
  - [ ] Setup CI/CD (+code quality + coverage tooling)
86
93
  - [ ] Setup website
87
94
  - [ ] Create a JVM compatible version
88
95
  - [x] Create interface to force implementation of required properties and make it more stable
89
96
  - [x] Improve error handling (missing properties etc.)
97
+ - [x] Support dynamic responses via request/response context
90
98
  - [ ] Create an optional persistent state
91
99
 
92
100
 
93
101
  ## Release notes (will be moved to GitHub in the future)
102
+ - v1.11.0 - Support dynamic responses via request/response context, added comprehensive tests, and switched to a faster build-and-serve workflow
103
+ - v1.10.0 - Improved path resolution and library usage support
94
104
  - v1.0.8 - Minor bugfixes
95
105
  - v1.0.7 - Minor bugfixes
96
106
  - v1.0.6 - Bugfix: accidentally included "npm" and "install" dependency, removed again
package/jest.config.js ADDED
@@ -0,0 +1,12 @@
1
+ const { createDefaultPreset } = require("ts-jest");
2
+
3
+ const tsJestTransformCfg = createDefaultPreset().transform;
4
+
5
+ /** @type {import("jest").Config} **/
6
+ module.exports = {
7
+ testEnvironment: "node",
8
+ testPathIgnorePatterns: ["/node_modules/", "/dist/"],
9
+ transform: {
10
+ ...tsJestTransformCfg,
11
+ },
12
+ };
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "typescript-mock-server",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Simple mock server that can be used in front end development. Instead of creating json files you can just publish TypeScript objects as json",
5
5
  "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1",
7
- "example": "ts-node-dev src/index.ts --path=$INIT_CWD/tms-models --port=5200 --cors=http://localhost:5200",
8
- "start": "ts-node-dev src/index.ts",
6
+ "test": "jest",
7
+ "build": "tsc --outDir dist",
8
+ "example": "npm run build && node dist/src/index.js --path=tms-models --port=5200 --cors=http://localhost:5200",
9
+ "start": "npm run build && node dist/src/index.js",
9
10
  "update-deps": "npm update",
10
11
  "get-version": "echo $npm_package_version",
11
12
  "publish-to-npm": "git tag -a ${npm_package_version} -m \"v${npm_package_version}\" && git push origin ${npm_package_version} && npm publish"
@@ -9,7 +9,8 @@ import { opendir } from 'fs/promises';
9
9
  import { LoggerImpl } from './logger-impl';
10
10
  import { Logger } from '../logger';
11
11
  import { TypescriptMockServer } from '../typescript-mock-server';
12
- import { Interval } from '../models/config';
12
+ import { Interval, MockModel, ServerConfig } from '../models/config';
13
+ import path from 'path';
13
14
 
14
15
  export class TypescriptMockServerImpl implements TypescriptMockServer{
15
16
 
@@ -19,12 +20,21 @@ export class TypescriptMockServerImpl implements TypescriptMockServer{
19
20
  private readonly basePath;
20
21
  private registeredEndpoints: RegisteredEndpoint[] = [];
21
22
 
22
- constructor() {
23
- this.app = express();
24
- this.basePath = this.getPath();
23
+ constructor(config?: ServerConfig | Express) {
24
+ if (config && 'use' in (config as any)) {
25
+ this.app = config as Express;
26
+ this.basePath = this.getPath();
27
+ } else {
28
+ const serverConfig = config as ServerConfig;
29
+ this.app = serverConfig?.app || express();
30
+ this.basePath = this.getPath(serverConfig?.path);
31
+ }
25
32
  }
26
33
 
27
34
  private static async loadModule(moduleName: string) {
35
+ if (moduleName.endsWith('.ts')) {
36
+ require('ts-node').register({ transpileOnly: true });
37
+ }
28
38
  return await import(moduleName);
29
39
  }
30
40
 
@@ -36,43 +46,55 @@ export class TypescriptMockServerImpl implements TypescriptMockServer{
36
46
  };
37
47
 
38
48
  this.app.use(cors(corsSetting))
49
+ // add started endpoint
50
+ this.addEndpoint('state', 'get', { data: { status: 'started' } });
51
+
39
52
  await this.readRoutes(this.basePath).catch(error => this.log.error(error));
40
53
  this.app.listen(port, () => {
41
54
  this.log.info(`App is listening on port ${port}!`);
42
55
  });
43
56
 
44
- // add started endpoint
45
- this.addEndpoint('state', 'get', { data: "{\"status\": \"started\"}" });
46
- this.log.info(`Started mock server on port ${this.commandLine.getCommand(Command.PORT)}`);
57
+ this.log.info(`Started mock server on port ${port}`);
47
58
  }
48
59
 
49
- private async readRoutes(path: string) {
50
- const dir = await opendir(path);
60
+ private async readRoutes(dirPath: string) {
61
+ const dir = await opendir(dirPath);
51
62
  for await (const dirent of dir) {
52
63
  if (dirent.isDirectory()) {
53
- await this.readRoutes(`${path}/${dirent.name}`);
64
+ await this.readRoutes(`${dirPath}/${dirent.name}`);
54
65
  } else {
55
- this.handleFile(path, dirent);
66
+ await this.handleFile(dirPath, dirent);
56
67
  }
57
68
  }
58
- this.registeredEndpoints.forEach(endpoint => this.log.info(`${endpoint.httpVerb.toUpperCase()} http://localhost:${this.commandLine.getCommand(Command.PORT)}${endpoint.endpoint}`));
69
+ const port = this.commandLine.getCommand(Command.PORT) || 3000;
70
+ this.registeredEndpoints.forEach(endpoint => this.log.info(`${endpoint.httpVerb.toUpperCase()} http://localhost:${port}${endpoint.endpoint}`));
59
71
  this.registeredEndpoints = [];
60
72
  }
61
73
 
62
- private handleFile(path: string, dirent: Dirent) {
74
+ private async handleFile(dirPath: string, dirent: Dirent) {
63
75
  const httpVerb = (dirent.name.indexOf('-') > -1 ? dirent.name.split('-')[0] : dirent.name.split('.')[0]) as HttpVerb;
64
- this.handleRequest(path, dirent, httpVerb);
76
+ await this.handleRequest(dirPath, dirent, httpVerb);
65
77
  }
66
78
 
67
- private addEndpoint(endpoint: string, httpVerb: HttpVerb, model: any) {
68
- this.app[httpVerb](endpoint, (req, res) => {
69
- if (model?.config?.statusCode) {
70
- res.statusCode = model?.config?.statusCode;
79
+ private addEndpoint(endpoint: string, httpVerb: HttpVerb, model: MockModel) {
80
+ const route = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
81
+ this.app[httpVerb](route, (req: any, res: any) => {
82
+ let responseData = model.data;
83
+ let statusCode = model?.config?.statusCode;
84
+ let delay = model?.config?.delay;
85
+
86
+ if (typeof model.data === 'function') {
87
+ responseData = model.data(req, res);
88
+ }
89
+
90
+ if (statusCode) {
91
+ res.statusCode = statusCode;
71
92
  }
72
- if (model?.config?.delay) {
73
- setTimeout(() => res.send(model.data), this.getDelayValue(model?.config?.delay));
93
+
94
+ if (delay) {
95
+ setTimeout(() => res.send(responseData), this.getDelayValue(delay));
74
96
  } else {
75
- return res.send(model.data);
97
+ return res.send(responseData);
76
98
  }
77
99
  });
78
100
  }
@@ -86,17 +108,30 @@ export class TypescriptMockServerImpl implements TypescriptMockServer{
86
108
  return 0;
87
109
  }
88
110
 
89
- private handleRequest(path: string, dirent: Dirent, httpVerb: HttpVerb) {
90
- const endpoint = this.convertFileNameToEndpoint(path, dirent, httpVerb);
91
- const modulePath = `${path}/${dirent.name}`;
111
+ private async handleRequest(dirPath: string, dirent: Dirent, httpVerb: HttpVerb) {
112
+ const endpoint = this.convertFileNameToEndpoint(dirPath, dirent, httpVerb);
113
+ let modulePath = `${dirPath}/${dirent.name}`;
92
114
  this.registeredEndpoints.push({ httpVerb, endpoint });
93
- TypescriptMockServerImpl.loadModule(modulePath)
115
+
116
+ if (__filename.endsWith('.js') && modulePath.endsWith('.ts')) {
117
+ const distPath = path.join(process.cwd(), 'dist');
118
+ const potentialJsPath = modulePath
119
+ .replace(process.cwd(), distPath)
120
+ .replace(/\.ts$/, '.js');
121
+
122
+ const fs = require('fs');
123
+ if (fs.existsSync(potentialJsPath)) {
124
+ modulePath = potentialJsPath;
125
+ }
126
+ }
127
+
128
+ await TypescriptMockServerImpl.loadModule(modulePath)
94
129
  .then(model => this.addEndpoint(endpoint, httpVerb, model))
95
130
  .catch(error => this.log.error(error));
96
131
  }
97
132
 
98
- private convertFileNameToEndpoint(path: string, dirent: Dirent, httpVerb: HttpVerb): string {
99
- const endpoint = `${path.replace(this.basePath, '')}/${dirent.name}`
133
+ private convertFileNameToEndpoint(dirPath: string, dirent: Dirent, httpVerb: HttpVerb): string {
134
+ const endpoint = `${dirPath.replace(this.basePath, '')}/${dirent.name}`
100
135
  .replace('.ts', '')
101
136
  .replace(`${httpVerb}-`, '')
102
137
  .replace(httpVerb, '');
@@ -107,11 +142,17 @@ export class TypescriptMockServerImpl implements TypescriptMockServer{
107
142
  return endpoint;
108
143
  }
109
144
 
110
- private getPath(): string {
145
+ private getPath(defaultPath: string = 'tms-models'): string {
146
+ let definedPath = defaultPath;
111
147
  if (!this.commandLine.getCommands().has(Command.PATH)) {
112
- this.log.warn(`Path parameter not set, fallback to default tms-models`);
113
- return 'tms-models';
148
+ this.log.warn(`Path parameter not set, fallback to default ${defaultPath}`);
149
+ } else {
150
+ definedPath = this.commandLine.getCommand(Command.PATH)!!;
151
+ }
152
+
153
+ if (path.isAbsolute(definedPath)) {
154
+ return definedPath;
114
155
  }
115
- return this.commandLine.getCommand(Command.PATH)!!;
156
+ return path.join(process.cwd(), definedPath);
116
157
  }
117
158
  }
@@ -7,7 +7,17 @@ export interface RequestConfig {
7
7
  statusCode?: number; // Status code of response
8
8
  }
9
9
 
10
+ export interface MockModel {
11
+ data: any | ((req: any, res: any) => any);
12
+ config?: RequestConfig;
13
+ }
14
+
10
15
  export interface Interval {
11
16
  min: number; // Minimum boundary, including the value
12
17
  max: number; // Maximum boundary, including the value
13
18
  }
19
+
20
+ export interface ServerConfig {
21
+ app?: any; // Express app instance
22
+ path?: string; // Path to models folder
23
+ }
@@ -0,0 +1,49 @@
1
+ import { CommandLineImpl } from '../../src/impl/command-line-impl';
2
+ import { Command } from '../../src/command-line';
3
+
4
+ describe('CommandLineImpl', () => {
5
+ const originalArgv = process.argv;
6
+
7
+ beforeEach(() => {
8
+ jest.resetModules();
9
+ });
10
+
11
+ afterEach(() => {
12
+ process.argv = originalArgv;
13
+ });
14
+
15
+ it('should parse command line arguments correctly', () => {
16
+ process.argv = ['node', 'index.js', '--port=4000', '--path=./test-models', '--cors=http://localhost:3000'];
17
+ const commandLine = new CommandLineImpl();
18
+
19
+ expect(commandLine.getCommand(Command.PORT)).toBe('4000');
20
+ expect(commandLine.getCommand(Command.PATH)).toBe('./test-models');
21
+ expect(commandLine.getCommand(Command.CORS)).toBe('http://localhost:3000');
22
+ });
23
+
24
+ it('should handle arguments with quotes', () => {
25
+ process.argv = ['node', 'index.js', '--path="C:/Program Files/Models"', "--cors='*'", '--port=5000'];
26
+ const commandLine = new CommandLineImpl();
27
+
28
+ expect(commandLine.getCommand(Command.PATH)).toBe('C:/Program Files/Models');
29
+ expect(commandLine.getCommand(Command.CORS)).toBe('*');
30
+ expect(commandLine.getCommand(Command.PORT)).toBe('5000');
31
+ });
32
+
33
+ it('should return undefined for missing commands', () => {
34
+ process.argv = ['node', 'index.js'];
35
+ const commandLine = new CommandLineImpl();
36
+
37
+ expect(commandLine.getCommand(Command.PORT)).toBeUndefined();
38
+ });
39
+
40
+ it('should return all commands', () => {
41
+ process.argv = ['node', 'index.js', '--port=4000', '--path=./models'];
42
+ const commandLine = new CommandLineImpl();
43
+ const commands = commandLine.getCommands();
44
+
45
+ expect(commands.size).toBe(2);
46
+ expect(commands.get(Command.PORT)).toBe('4000');
47
+ expect(commands.get(Command.PATH)).toBe('./models');
48
+ });
49
+ });
@@ -0,0 +1,181 @@
1
+ import request from 'supertest';
2
+ import express, { Express } from 'express';
3
+ import { TypescriptMockServerImpl } from '../../src/impl/typescript-mock-server-impl';
4
+ import * as fsPromises from 'fs/promises';
5
+ import { Command } from '../../src/command-line';
6
+ import path from 'path';
7
+
8
+ jest.mock('fs/promises');
9
+
10
+ describe('TypescriptMockServerImpl', () => {
11
+ let server: TypescriptMockServerImpl;
12
+ let app: Express;
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ app = express();
17
+ // Spy on listen to prevent it from actually starting
18
+ jest.spyOn(app, 'listen').mockImplementation((port: any, callback: any) => {
19
+ if (callback) callback();
20
+ return {} as any;
21
+ });
22
+ // Reset process.argv
23
+ process.argv = ['node', 'index.js'];
24
+ });
25
+
26
+ it('should start and have the state endpoint', async () => {
27
+ (fsPromises.opendir as jest.Mock).mockResolvedValue(async function* () {
28
+ // yield nothing
29
+ }());
30
+
31
+ server = new TypescriptMockServerImpl(app);
32
+ await server.start();
33
+
34
+ const response = await request(app).get('/state');
35
+ expect(response.status).toBe(200);
36
+ expect(response.body).toEqual({ status: 'started' });
37
+ });
38
+
39
+ it('should have the state endpoint even if other routes are loaded', async () => {
40
+ (fsPromises.opendir as jest.Mock).mockResolvedValue(async function* () {
41
+ // yield nothing
42
+ }());
43
+
44
+ server = new TypescriptMockServerImpl(app);
45
+ await server.start();
46
+
47
+ const response = await request(app).get('/state');
48
+ expect(response.status).toBe(200);
49
+ });
50
+
51
+ it('should load routes from directory', async () => {
52
+ const mockFiles = [
53
+ { name: 'get-test.ts', isDirectory: () => false },
54
+ { name: 'post-data.ts', isDirectory: () => false }
55
+ ];
56
+
57
+ (fsPromises.opendir as jest.Mock).mockResolvedValue(async function* () {
58
+ for (const file of mockFiles) {
59
+ yield file;
60
+ }
61
+ }());
62
+
63
+ // Mock loadModule
64
+ const loadModuleSpy = jest.spyOn(TypescriptMockServerImpl as any, 'loadModule');
65
+ loadModuleSpy.mockImplementation(async (...args: any[]) => {
66
+ const path = args[0] as string;
67
+ if (path.includes('get-test')) {
68
+ return { data: { message: 'get success' } };
69
+ }
70
+ if (path.includes('post-data')) {
71
+ return { data: { message: 'post success' } };
72
+ }
73
+ return {};
74
+ });
75
+
76
+ server = new TypescriptMockServerImpl(app);
77
+ await server.start();
78
+
79
+ const getResponse = await request(app).get('/test');
80
+ expect(getResponse.status).toBe(200);
81
+ expect(getResponse.body).toEqual({ message: 'get success' });
82
+
83
+ const postResponse = await request(app).post('/data');
84
+ expect(postResponse.status).toBe(200);
85
+ expect(postResponse.body).toEqual({ message: 'post success' });
86
+ });
87
+
88
+ it('should handle status codes and delay from config', async () => {
89
+ const mockFiles = [
90
+ { name: 'get-config.ts', isDirectory: () => false }
91
+ ];
92
+
93
+ (fsPromises.opendir as jest.Mock).mockResolvedValue(async function* () {
94
+ yield mockFiles[0];
95
+ }());
96
+
97
+ const loadModuleSpy = jest.spyOn(TypescriptMockServerImpl as any, 'loadModule');
98
+ loadModuleSpy.mockImplementation(async () => {
99
+ return {
100
+ data: { message: 'custom' },
101
+ config: { statusCode: 201, delay: 10 }
102
+ } as any;
103
+ });
104
+
105
+ server = new TypescriptMockServerImpl(app);
106
+ await server.start();
107
+
108
+ const response = await request(app).get('/config');
109
+ expect(response.status).toBe(201);
110
+ expect(response.body).toEqual({ message: 'custom' });
111
+ });
112
+
113
+ it('should resolve absolute paths correctly', async () => {
114
+ (fsPromises.opendir as jest.Mock).mockResolvedValue(async function* () {
115
+ // yield nothing
116
+ }());
117
+
118
+ const absolutePath = path.resolve('/absolute/path/to/models');
119
+ process.argv = ['node', 'index.js', `--path=${absolutePath}`];
120
+
121
+ server = new TypescriptMockServerImpl(app);
122
+ // @ts-ignore
123
+ expect(server.basePath).toBe(absolutePath);
124
+ });
125
+
126
+ it('should resolve relative paths from process.cwd()', async () => {
127
+ (fsPromises.opendir as jest.Mock).mockResolvedValue(async function* () {
128
+ // yield nothing
129
+ }());
130
+
131
+ const relativePath = 'custom-models';
132
+ process.argv = ['node', 'index.js', `--path=${relativePath}`];
133
+
134
+ server = new TypescriptMockServerImpl(app);
135
+ // @ts-ignore
136
+ expect(server.basePath).toBe(path.join(process.cwd(), relativePath));
137
+ });
138
+
139
+ it('should use custom default path from config', async () => {
140
+ (fsPromises.opendir as jest.Mock).mockResolvedValue(async function* () {
141
+ // yield nothing
142
+ }());
143
+
144
+ const customDefault = 'my-custom-models';
145
+ server = new TypescriptMockServerImpl({ path: customDefault, app });
146
+ // @ts-ignore
147
+ expect(server.basePath).toBe(path.join(process.cwd(), customDefault));
148
+ });
149
+
150
+ it('should support dynamic data as a function receiving req and res', async () => {
151
+ const mockFiles = [
152
+ { name: 'get-dynamic.ts', isDirectory: () => false }
153
+ ];
154
+
155
+ (fsPromises.opendir as jest.Mock).mockResolvedValue(async function* () {
156
+ yield mockFiles[0];
157
+ }());
158
+
159
+ const loadModuleSpy = jest.spyOn(TypescriptMockServerImpl as any, 'loadModule');
160
+ loadModuleSpy.mockImplementation(async () => {
161
+ return {
162
+ data: (req: any, res: any) => {
163
+ return {
164
+ query: req.query.q,
165
+ method: req.method
166
+ };
167
+ }
168
+ };
169
+ });
170
+
171
+ server = new TypescriptMockServerImpl(app);
172
+ await server.start();
173
+
174
+ const response = await request(app).get('/dynamic?q=test');
175
+ expect(response.status).toBe(200);
176
+ expect(response.body).toEqual({
177
+ query: 'test',
178
+ method: 'GET'
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,22 @@
1
+ import { Request, Response } from 'express';
2
+ import { MockModel, RequestConfig } from '../../src/models/config';
3
+
4
+ export interface User {
5
+ id: number;
6
+ username: string;
7
+ email: string;
8
+ }
9
+
10
+ export const data = (req: Request, res: Response): User => {
11
+ const userId = req.query.id ? parseInt(req.query.id as string) : 1;
12
+
13
+ return {
14
+ id: userId,
15
+ username: `user_${userId}`,
16
+ email: `user_${userId}@example.com`
17
+ };
18
+ };
19
+
20
+ export const config: RequestConfig = {
21
+ statusCode: 200
22
+ };
package/tsconfig.json CHANGED
@@ -15,7 +15,11 @@
15
15
 
16
16
  "skipLibCheck": true /* Skip type checking all .d.ts files. */
17
17
  },
18
+ "include": [
19
+ "src/**/*"
20
+ ],
18
21
  "exclude": [
19
- "node_modules"
22
+ "node_modules",
23
+ "test"
20
24
  ]
21
25
  }