some more
This commit is contained in:
parent
10646b1c25
commit
74678855cf
|
|
@ -45,6 +45,7 @@ import { findValidator, ValidationError, type ValidatorContext } from '../protoc
|
||||||
import { createInstrumentation } from './clientInstrumentation';
|
import { createInstrumentation } from './clientInstrumentation';
|
||||||
import type { ClientInstrumentation } from './clientInstrumentation';
|
import type { ClientInstrumentation } from './clientInstrumentation';
|
||||||
import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
|
import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
|
||||||
|
import { MockingProxy } from './mockingProxy';
|
||||||
|
|
||||||
class Root extends ChannelOwner<channels.RootChannel> {
|
class Root extends ChannelOwner<channels.RootChannel> {
|
||||||
constructor(connection: Connection) {
|
constructor(connection: Connection) {
|
||||||
|
|
@ -279,6 +280,9 @@ export class Connection extends EventEmitter {
|
||||||
if (!this._localUtils)
|
if (!this._localUtils)
|
||||||
this._localUtils = result as LocalUtils;
|
this._localUtils = result as LocalUtils;
|
||||||
break;
|
break;
|
||||||
|
case 'MockingProxy':
|
||||||
|
result = new MockingProxy(parent, type, guid, initializer);
|
||||||
|
break;
|
||||||
case 'Page':
|
case 'Page':
|
||||||
result = new Page(parent, type, guid, initializer);
|
result = new Page(parent, type, guid, initializer);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import type { Size } from './types';
|
import type { Size } from './types';
|
||||||
import { APIRequestContext } from './fetch';
|
|
||||||
|
|
||||||
type DeviceDescriptor = {
|
type DeviceDescriptor = {
|
||||||
userAgent: string,
|
userAgent: string,
|
||||||
|
|
@ -31,7 +30,6 @@ type Devices = { [name: string]: DeviceDescriptor };
|
||||||
|
|
||||||
export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
||||||
readonly devices: Devices;
|
readonly devices: Devices;
|
||||||
readonly requestContext: APIRequestContext;
|
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
|
|
@ -39,6 +37,5 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
||||||
this.devices = {};
|
this.devices = {};
|
||||||
for (const { name, descriptor } of initializer.deviceDescriptors)
|
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||||
this.devices[name] = descriptor;
|
this.devices[name] = descriptor;
|
||||||
this.requestContext = APIRequestContext.from(initializer.requestContext);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import * as network from './network';
|
||||||
import { urlMatches, urlMatchesEqual, type URLMatch } from '../utils/isomorphic/urlMatch';
|
import { urlMatches, urlMatchesEqual, type URLMatch } from '../utils/isomorphic/urlMatch';
|
||||||
import type { LocalUtils } from './localUtils';
|
import type { LocalUtils } from './localUtils';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { EventEmitter } from './eventEmitter';
|
|
||||||
import type { WaitForEventOptions } from './types';
|
import type { WaitForEventOptions } from './types';
|
||||||
import { Waiter } from './waiter';
|
import { Waiter } from './waiter';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
|
|
@ -26,31 +25,34 @@ import { isString } from '../utils/isomorphic/stringUtils';
|
||||||
import { isRegExp } from '../utils';
|
import { isRegExp } from '../utils';
|
||||||
import { trimUrl } from './page';
|
import { trimUrl } from './page';
|
||||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||||
|
import { ChannelOwner } from './channelOwner';
|
||||||
|
import { APIRequestContext } from './fetch';
|
||||||
|
|
||||||
export class MockingProxyFactory implements api.MockingProxyFactory {
|
export class MockingProxyFactory implements api.MockingProxyFactory {
|
||||||
constructor(private _localUtils: LocalUtils) {}
|
constructor(private _localUtils: LocalUtils) {}
|
||||||
async newProxy(port: number): Promise<api.MockingProxy> {
|
async newProxy(port?: number): Promise<api.MockingProxy> {
|
||||||
return await MockingProxy.create(this._localUtils, port);
|
const { mockingProxy } = await this._localUtils._channel.newMockingProxy({ port });
|
||||||
|
return (mockingProxy as any)._object;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockingProxy extends EventEmitter implements api.MockingProxy {
|
export class MockingProxy extends ChannelOwner<channels.MockingProxyChannel> implements api.MockingProxy {
|
||||||
_routes: network.RouteHandler[] = [];
|
private _routes: network.RouteHandler[] = [];
|
||||||
private _localUtils: LocalUtils;
|
|
||||||
private _port: number;
|
private _port: number;
|
||||||
private _timeoutSettings = new TimeoutSettings();
|
private _timeoutSettings = new TimeoutSettings();
|
||||||
|
private _requestContext: APIRequestContext;
|
||||||
|
|
||||||
private routeListener = ({ route }: channels.LocalUtilsRouteEvent) => {
|
private routeListener = ({ route }: channels.MockingProxyRouteEvent) => {
|
||||||
this._onRoute(network.Route.from(route));
|
this._onRoute(network.Route.from(route));
|
||||||
};
|
};
|
||||||
private failedListener = (params: channels.LocalUtilsRequestFailedEvent) => {
|
private failedListener = (params: channels.MockingProxyRequestFailedEvent) => {
|
||||||
const request = network.Request.from(params.request);
|
const request = network.Request.from(params.request);
|
||||||
if (params.failureText)
|
if (params.failureText)
|
||||||
request._failureText = params.failureText;
|
request._failureText = params.failureText;
|
||||||
request._setResponseEndTiming(params.responseEndTiming);
|
request._setResponseEndTiming(params.responseEndTiming);
|
||||||
this.emit('requestfailed', request);
|
this.emit('requestfailed', request);
|
||||||
};
|
};
|
||||||
private finishedListener = (params: channels.LocalUtilsRequestFinishedEvent) => {
|
private finishedListener = (params: channels.MockingProxyRequestFinishedEvent) => {
|
||||||
const { responseEndTiming } = params;
|
const { responseEndTiming } = params;
|
||||||
const request = network.Request.from(params.request);
|
const request = network.Request.from(params.request);
|
||||||
const response = network.Response.fromNullable(params.response);
|
const response = network.Response.fromNullable(params.response);
|
||||||
|
|
@ -58,42 +60,32 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy {
|
||||||
this.emit('requestfinished', request);
|
this.emit('requestfinished', request);
|
||||||
response?._finishedPromise.resolve(null);
|
response?._finishedPromise.resolve(null);
|
||||||
};
|
};
|
||||||
private responseListener = ({ response }: channels.LocalUtilsResponseEvent) => {
|
private responseListener = ({ response }: channels.MockingProxyResponseEvent) => {
|
||||||
this.emit('response', network.Response.from(response));
|
this.emit('response', network.Response.from(response));
|
||||||
};
|
};
|
||||||
private requestListener = ({ request }: channels.LocalUtilsRequestEvent) => {
|
private requestListener = ({ request }: channels.MockingProxyRequestEvent) => {
|
||||||
this.emit('request', network.Request.from(request));
|
this.emit('request', network.Request.from(request));
|
||||||
};
|
};
|
||||||
|
|
||||||
private constructor(localUtils: LocalUtils, port: number) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.MockingProxyInitializer) {
|
||||||
super();
|
super(parent, type, guid, initializer);
|
||||||
|
|
||||||
this._localUtils = localUtils;
|
this._port = initializer.port;
|
||||||
this._port = port;
|
this._requestContext = APIRequestContext.from(initializer.requestContext);
|
||||||
|
|
||||||
this._localUtils._channel.on('route', this.routeListener);
|
this._channel.on('route', this.routeListener);
|
||||||
this._localUtils._channel.on('request', this.requestListener);
|
this._channel.on('request', this.requestListener);
|
||||||
this._localUtils._channel.on('requestFailed', this.failedListener);
|
this._channel.on('requestFailed', this.failedListener);
|
||||||
this._localUtils._channel.on('requestFinished', this.finishedListener);
|
this._channel.on('requestFinished', this.finishedListener);
|
||||||
this._localUtils._channel.on('response', this.responseListener);
|
this._channel.on('response', this.responseListener);
|
||||||
}
|
|
||||||
|
|
||||||
private async _start() {
|
|
||||||
await this._localUtils._channel.setServerNetworkInterceptionPatterns({ patterns: [], port: this._port });
|
|
||||||
}
|
|
||||||
|
|
||||||
static async create(localUtils: LocalUtils, port: number) {
|
|
||||||
const instance = new MockingProxy(localUtils, port);
|
|
||||||
await instance._start();
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this._localUtils._channel.off('route', this.routeListener);
|
this._channel.off('route', this.routeListener);
|
||||||
this._localUtils._channel.off('request', this.requestListener);
|
this._channel.off('request', this.requestListener);
|
||||||
this._localUtils._channel.off('requestFailed', this.failedListener);
|
this._channel.off('requestFailed', this.failedListener);
|
||||||
this._localUtils._channel.off('requestFinished', this.finishedListener);
|
this._channel.off('requestFinished', this.finishedListener);
|
||||||
this._localUtils._channel.off('response', this.responseListener);
|
this._channel.off('response', this.responseListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
|
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
|
||||||
|
|
@ -127,7 +119,7 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onRoute(route: network.Route) {
|
async _onRoute(route: network.Route) {
|
||||||
route._request = this._localUtils.requestContext;
|
route._request = this._requestContext;
|
||||||
const routeHandlers = this._routes.slice();
|
const routeHandlers = this._routes.slice();
|
||||||
for (const routeHandler of routeHandlers) {
|
for (const routeHandler of routeHandlers) {
|
||||||
if (!routeHandler.matches(route.request().url()))
|
if (!routeHandler.matches(route.request().url()))
|
||||||
|
|
@ -149,8 +141,7 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _updateInterceptionPatterns() {
|
private async _updateInterceptionPatterns() {
|
||||||
const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes);
|
await this._channel.setInterceptionPatterns({ patterns: network.RouteHandler.prepareInterceptionPatterns(this._routes) });
|
||||||
await this._localUtils._channel.setServerNetworkInterceptionPatterns({ patterns, port: this._port });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<network.Request> {
|
async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<network.Request> {
|
||||||
|
|
@ -182,10 +173,10 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> {
|
private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> {
|
||||||
return await this._localUtils._wrapApiCall(async () => {
|
return await this._wrapApiCall(async () => {
|
||||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||||
const waiter = Waiter.createForEvent(this._localUtils, event);
|
const waiter = Waiter.createForEvent(this, event);
|
||||||
if (logLine)
|
if (logLine)
|
||||||
waiter.log(logLine);
|
waiter.log(logLine);
|
||||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@ import { playwrightTest as baseTest, expect } from '../config/browserTest';
|
||||||
import { pipeline } from 'stream/promises';
|
import { pipeline } from 'stream/promises';
|
||||||
import { suppressCertificateWarning } from 'tests/config/utils';
|
import { suppressCertificateWarning } from 'tests/config/utils';
|
||||||
|
|
||||||
const test = baseTest.extend<{ proxiedRequest: APIRequestContext }, { mockingProxy: MockingProxy }>({
|
const test = baseTest.extend<{ proxiedRequest: APIRequestContext }, { mockproxy: MockingProxy }>({
|
||||||
mockingProxy: [async ({ playwright }, use, testInfo) => {
|
mockproxy: [async ({ playwright }, use, testInfo) => {
|
||||||
const port = 32181 + testInfo.parallelIndex;
|
const port = 32181 + testInfo.parallelIndex;
|
||||||
const proxy = await playwright.mockingProxy.newProxy(port);
|
const proxy = await playwright.mockingProxy.newProxy(port);
|
||||||
await use(proxy);
|
await use(proxy);
|
||||||
}, { scope: 'worker' }],
|
}, { scope: 'worker' }],
|
||||||
proxiedRequest: async ({ request, mockingProxy: mockproxy }, use) => {
|
proxiedRequest: async ({ request, mockproxy }, use) => {
|
||||||
const originalFetch = request.fetch;
|
const originalFetch = request.fetch;
|
||||||
request.fetch = function(urlOrRequest, options) {
|
request.fetch = function(urlOrRequest, options) {
|
||||||
if (typeof urlOrRequest !== 'string')
|
if (typeof urlOrRequest !== 'string')
|
||||||
|
|
@ -36,12 +36,12 @@ const test = baseTest.extend<{ proxiedRequest: APIRequestContext }, { mockingPro
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async ({ mockingProxy: mockproxy }) => {
|
test.beforeEach(async ({ mockproxy }) => {
|
||||||
await mockproxy.unrouteAll();
|
await mockproxy.unrouteAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('transparent', () => {
|
test.describe('transparent', () => {
|
||||||
test('generates events', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('generates events', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
const events: string[] = [];
|
const events: string[] = [];
|
||||||
mockproxy.on('request', () => {
|
mockproxy.on('request', () => {
|
||||||
events.push('request');
|
events.push('request');
|
||||||
|
|
@ -58,7 +58,7 @@ test.describe('transparent', () => {
|
||||||
expect(events).toEqual(['request', 'response', 'requestfinished']);
|
expect(events).toEqual(['request', 'response', 'requestfinished']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('event properties', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('event properties', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
const [
|
const [
|
||||||
requestFinished,
|
requestFinished,
|
||||||
request,
|
request,
|
||||||
|
|
@ -122,7 +122,7 @@ test.describe('transparent', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('securityDetails', async ({ httpsServer, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('securityDetails', async ({ httpsServer, proxiedRequest, mockproxy }) => {
|
||||||
const oldValue = process.env['NODE_TLS_REJECT_UNAUTHORIZED'];
|
const oldValue = process.env['NODE_TLS_REJECT_UNAUTHORIZED'];
|
||||||
// https://stackoverflow.com/a/21961005/552185
|
// https://stackoverflow.com/a/21961005/552185
|
||||||
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
|
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
|
||||||
|
|
@ -149,7 +149,7 @@ test.describe('transparent', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('request with body', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('request with body', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
server.setRoute('/echo', (req, res) => pipeline(req, res));
|
server.setRoute('/echo', (req, res) => pipeline(req, res));
|
||||||
const [
|
const [
|
||||||
requestEvent,
|
requestEvent,
|
||||||
|
|
@ -173,7 +173,7 @@ test.describe('transparent', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('request failed', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('request failed', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
server.setRoute('/failure', (req, res) => {
|
server.setRoute('/failure', (req, res) => {
|
||||||
res.socket.destroy();
|
res.socket.destroy();
|
||||||
});
|
});
|
||||||
|
|
@ -196,14 +196,14 @@ test.describe('transparent', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stalling', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('stalling', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
const routes: Route[] = [];
|
const routes: Route[] = [];
|
||||||
await mockproxy.route('**/abort', route => routes.push(route));
|
await mockproxy.route('**/abort', route => routes.push(route));
|
||||||
await expect(() => proxiedRequest.get(server.PREFIX + '/abort', { timeout: 100 })).rejects.toThrowError('Request timed out after 100ms');
|
await expect(() => proxiedRequest.get(server.PREFIX + '/abort', { timeout: 100 })).rejects.toThrowError('Request timed out after 100ms');
|
||||||
expect(routes.length).toBe(1);
|
expect(routes.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('route properties', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('route properties', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
const routes: Route[] = [];
|
const routes: Route[] = [];
|
||||||
await mockproxy.route('**/*', (route, request) => {
|
await mockproxy.route('**/*', (route, request) => {
|
||||||
expect(route.request()).toBe(request);
|
expect(route.request()).toBe(request);
|
||||||
|
|
@ -214,12 +214,12 @@ test('route properties', async ({ server, proxiedRequest, mockingProxy: mockprox
|
||||||
expect(routes.length).toBe(1);
|
expect(routes.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('aborting', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('aborting', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
await mockproxy.route('**/abort', route => route.abort());
|
await mockproxy.route('**/abort', route => route.abort());
|
||||||
await expect(() => proxiedRequest.get(server.PREFIX + '/abort', { timeout: 100 })).rejects.toThrowError('Request timed out after 100ms');
|
await expect(() => proxiedRequest.get(server.PREFIX + '/abort', { timeout: 100 })).rejects.toThrowError('Request timed out after 100ms');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fulfill', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('fulfill', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
let apiCalls = 0;
|
let apiCalls = 0;
|
||||||
server.setRoute('/endpoint', (req, res) => {
|
server.setRoute('/endpoint', (req, res) => {
|
||||||
apiCalls++;
|
apiCalls++;
|
||||||
|
|
@ -233,7 +233,7 @@ test('fulfill', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
||||||
expect(apiCalls).toBe(0);
|
expect(apiCalls).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('continue', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('continue', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
server.setRoute('/echo', (req, res) => {
|
server.setRoute('/echo', (req, res) => {
|
||||||
res.setHeader('request-method', req.method);
|
res.setHeader('request-method', req.method);
|
||||||
res.writeHead(200, req.headers);
|
res.writeHead(200, req.headers);
|
||||||
|
|
@ -255,7 +255,7 @@ test('continue', async ({ server, proxiedRequest, mockingProxy: mockproxy }) =>
|
||||||
expect(response.headers()['x-add']).toBe('baz');
|
expect(response.headers()['x-add']).toBe('baz');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fallback', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('fallback', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
server.setRoute('/foo', (req, res) => {
|
server.setRoute('/foo', (req, res) => {
|
||||||
res.end('ok');
|
res.end('ok');
|
||||||
});
|
});
|
||||||
|
|
@ -266,7 +266,7 @@ test('fallback', async ({ server, proxiedRequest, mockingProxy: mockproxy }) =>
|
||||||
expect(await response.text()).toBe('ok');
|
expect(await response.text()).toBe('ok');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fetch', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
|
test('fetch', async ({ server, proxiedRequest, mockproxy }) => {
|
||||||
server.setRoute('/foo', (req, res) => {
|
server.setRoute('/foo', (req, res) => {
|
||||||
res.end('ok');
|
res.end('ok');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue