diff --git a/CODEOWNERS b/CODEOWNERS
index f6db6c9ab2db..33a3b7d27dcf 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -9,6 +9,7 @@
packages/authentication/* @bajtos @raymondfeng
packages/boot/* @raymondfeng @virkt25
packages/build/* @bajtos @raymondfeng
+packages/http-caching-proxy/* @bajtos
packages/cli/* @raymondfeng @shimks
packages/context/* @bajtos @raymondfeng
packages/core/* @bajtos @raymondfeng
diff --git a/docs/apidocs.html b/docs/apidocs.html
index 5ea823019418..f64898157a70 100644
--- a/docs/apidocs.html
+++ b/docs/apidocs.html
@@ -19,6 +19,7 @@
List of packages
- @loopback/authentication
- @loopback/boot
+ - @loopback/caching-proxy
- @loopback/context
- @loopback/core
- @loopback/metadata
diff --git a/docs/site/MONOREPO.md b/docs/site/MONOREPO.md
index b8f12a805974..fd5b2e61fc2a 100644
--- a/docs/site/MONOREPO.md
+++ b/docs/site/MONOREPO.md
@@ -10,6 +10,7 @@ The [loopback-next](https://github.com/strongloop/loopback-next) repository uses
| [authentication](packages/authentication) | @loopback/authentication | A component for authentication support |
| [boot](packages/boot) | @loopback/boot | Convention based Bootstrapper and Booters |
| [build](packages/build) | @loopback/build | A set of common scripts and default configurations to build LoopBack 4 or other TypeScript modules |
+| [http-caching-proxy](packages/http-caching-proxy) | @loopback/http-caching-proxy | A caching HTTP proxy for integration tests. NOT SUITABLE FOR PRODUCTION USE!
| [cli](packages/cli) | @loopback/cli | CLI for LoopBack 4 |
| [context](packages/context) | @loopback/context | Facilities to manage artifacts and their dependencies in your Node.js applications. The module exposes TypeScript/JavaScript APIs and decorators to register artifacts, declare dependencies, and resolve artifacts by keys. It also serves as an IoC container to support dependency injection. |
| [core](packages/core) | @loopback/core | Define and implement core constructs such as Application and Component |
diff --git a/packages/http-caching-proxy/.npmrc b/packages/http-caching-proxy/.npmrc
new file mode 100644
index 000000000000..43c97e719a5a
--- /dev/null
+++ b/packages/http-caching-proxy/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/packages/http-caching-proxy/LICENSE b/packages/http-caching-proxy/LICENSE
new file mode 100644
index 000000000000..11e624fdc213
--- /dev/null
+++ b/packages/http-caching-proxy/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) IBM Corp. 2017,2018. All Rights Reserved.
+Node module: @loopback/caching-proxy
+This project is licensed under the MIT License, full text below.
+
+--------
+
+MIT license
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/packages/http-caching-proxy/README.md b/packages/http-caching-proxy/README.md
new file mode 100644
index 000000000000..b68ea30cba5d
--- /dev/null
+++ b/packages/http-caching-proxy/README.md
@@ -0,0 +1,101 @@
+# @loopback/http-caching-proxy
+
+A caching HTTP proxy for integration tests.
+
+**NOT SUITABLE FOR PRODUCTION USE!**
+
+## Overview
+
+Testing applications connecting to backend REST/SOAP services can be difficult:
+The backend service may be slow, apply rate limiting, etc. Integration tests
+become too slow in such case, which makes test-first development impractical.
+
+This can be addressed by setting up a snapshot-based mock server or using
+a caching HTTP client, but both of these solutions come with severe
+disadvantages:
+
+ - When using a snapshot-based mock server, we must ensure that snapshots
+ are up-to-date with the actual backend implementation.
+
+ - Caching at HTTP-client side requires non-trivial changes of the application
+ code.
+
+A filesystem-backed caching HTTP proxy offers a neat solution that combines
+caching and snapshots:
+
+ - The first request is forwarded to the actual backend and the response
+ is stored as a snapshot.
+ - Subsequent requests are served by the proxy using the cached snaphost.
+ - Snapshot older than a configured time are discarded and the first next
+ request will fetch the real response from the backend.
+
+## Installation
+
+```sh
+npm install --save-dev @loopback/http-caching-proxy
+```
+
+## Basic use
+
+Import the module at the top of your test file.
+
+```ts
+import {HttpCachingProxy} from '@loopback/http-caching-proxy';
+```
+
+Create a proxy instance during test-suite setup
+(typically in Mocha's `before` hook):
+
+```ts
+const proxy = new HttpCachingProxy({
+ // directory where to store recorded snapshots - required
+ cachePath: path.resolve(__dirname, '.proxy-cache'),
+ // port where to listen - 0 by default
+ port: 0,
+ // how often to re-validate snapshots (in milliseconds) - one day by default
+ ttl: 24*60*60*1000,
+});
+await proxy.start();
+```
+
+In your tests, configure the client library to use the caching proxy.
+Below is an example configuration for
+[request](https://www.npmjs.com/package/request):
+
+```ts
+request = request.defaults({
+ proxy: proxy.url,
+ // Disable tunneling of HTTPS requests - this is required for HTTPS!
+ tunnel: false
+});
+```
+
+Finally, stop the proxy when the test suite is done
+(typically in Mocha's `after` hook):
+
+```ts
+await proxy.stop();
+```
+
+## API Documentation
+
+See the auto-generated documentation at
+[loopback.io](http://apidocs.loopback.io/@loopback%2fdocs/caching-proxy.html)
+
+## Contributions
+
+- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md)
+- [Join the team](https://github.com/strongloop/loopback-next/issues/110)
+
+## Tests
+
+Run `npm test` from the root folder.
+
+## Contributors
+
+See
+[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors).
+
+## License
+
+MIT
diff --git a/packages/http-caching-proxy/docs.json b/packages/http-caching-proxy/docs.json
new file mode 100644
index 000000000000..0ec9f273a95a
--- /dev/null
+++ b/packages/http-caching-proxy/docs.json
@@ -0,0 +1,7 @@
+{
+ "content": [
+ "index.ts",
+ "src/caching-proxy.ts"
+ ],
+ "codeSectionDepth": 4
+}
diff --git a/packages/http-caching-proxy/index.d.ts b/packages/http-caching-proxy/index.d.ts
new file mode 100644
index 000000000000..68e093338902
--- /dev/null
+++ b/packages/http-caching-proxy/index.d.ts
@@ -0,0 +1,6 @@
+// Copyright IBM Corp. 2018. All Rights Reserved.
+// Node module: @loopback/caching-proxy
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+export * from './dist8';
diff --git a/packages/http-caching-proxy/index.js b/packages/http-caching-proxy/index.js
new file mode 100644
index 000000000000..b2fd17a9c58a
--- /dev/null
+++ b/packages/http-caching-proxy/index.js
@@ -0,0 +1,6 @@
+// Copyright IBM Corp. 2018. All Rights Reserved.
+// Node module: @loopback/caching-proxy
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+module.exports = require('@loopback/dist-util').loadDist(__dirname);
diff --git a/packages/http-caching-proxy/index.ts b/packages/http-caching-proxy/index.ts
new file mode 100644
index 000000000000..52800f151e77
--- /dev/null
+++ b/packages/http-caching-proxy/index.ts
@@ -0,0 +1,8 @@
+// Copyright IBM Corp. 2018. All Rights Reserved.
+// Node module: @loopback/caching-proxy
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+// DO NOT EDIT THIS FILE
+// Add any additional (re)exports to src/index.ts instead.
+export * from './src';
diff --git a/packages/http-caching-proxy/package.json b/packages/http-caching-proxy/package.json
new file mode 100644
index 000000000000..73d2b63b93a9
--- /dev/null
+++ b/packages/http-caching-proxy/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@loopback/http-caching-proxy",
+ "version": "0.1.0",
+ "description": "A caching HTTP proxy for integration tests. NOT SUITABLE FOR PRODUCTION USE!",
+ "engines": {
+ "node": ">=8"
+ },
+ "scripts": {
+ "build": "npm run build:dist8 && npm run build:dist10",
+ "build:apidocs": "lb-apidocs",
+ "build:current": "lb-tsc",
+ "build:dist8": "lb-tsc es2017",
+ "build:dist10": "lb-tsc es2018",
+ "clean": "lb-clean loopback-caching-proxy*.tgz dist* package api-docs",
+ "pretest": "npm run build:current",
+ "test": "lb-mocha \"DIST/test/integration/**/*.js\"",
+ "verify": "npm pack && tar xf loopback-caching-proxy*.tgz && tree package && npm run clean"
+ },
+ "author": "IBM",
+ "copyright.owner": "IBM Corp.",
+ "license": "MIT",
+ "dependencies": {
+ "cacache": "^11.0.2",
+ "debug": "^3.1.0",
+ "p-event": "^1.3.0",
+ "request": "^2.87.0",
+ "request-promise-native": "^1.0.5",
+ "rimraf": "^2.6.2"
+ },
+ "devDependencies": {
+ "@loopback/build": "^0.6.5",
+ "@loopback/testlab": "^0.10.4",
+ "@types/debug": "^0.0.30",
+ "@types/delay": "^2.0.1",
+ "@types/node": "^10.1.1",
+ "@types/p-event": "^1.3.0",
+ "@types/request-promise-native": "^1.0.14",
+ "@types/rimraf": "^2.0.2",
+ "delay": "^3.0.0"
+ },
+ "keywords": [
+ "LoopBack",
+ "HTTP",
+ "Proxy",
+ "Cache",
+ "Test"
+ ],
+ "files": [
+ "README.md",
+ "index.js",
+ "index.d.ts",
+ "dist*/src",
+ "dist*/index*",
+ "src"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/strongloop/loopback-next.git"
+ }
+}
diff --git a/packages/http-caching-proxy/src/http-caching-proxy.ts b/packages/http-caching-proxy/src/http-caching-proxy.ts
new file mode 100644
index 000000000000..25f9868a175c
--- /dev/null
+++ b/packages/http-caching-proxy/src/http-caching-proxy.ts
@@ -0,0 +1,225 @@
+// Copyright IBM Corp. 2018. All Rights Reserved.
+// Node module: test-proxy
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+import * as debugFactory from 'debug';
+import {
+ OutgoingHttpHeaders,
+ Server as HttpServer,
+ ServerRequest,
+ ServerResponse,
+ createServer,
+} from 'http';
+import {AddressInfo} from 'net';
+import * as pEvent from 'p-event';
+import * as makeRequest from 'request-promise-native';
+
+const cacache = require('cacache');
+
+const debug = debugFactory('loopback:caching-proxy');
+
+export interface ProxyOptions {
+ /**
+ * Directory where to keep the cached snapshots.
+ */
+ cachePath: string;
+
+ /**
+ * How long to keep snapshots before making a new request to the backend.
+ * The value is in milliseconds.
+ *
+ * Default: one day
+ */
+ ttl?: number;
+
+ /**
+ * The port where the HTTP proxy should listen at.
+ * Default: 0 (let the system pick a free port)
+ */
+ port?: number;
+}
+
+const DEFAULT_OPTIONS = {
+ port: 0,
+ ttl: 24 * 60 * 60 * 1000,
+};
+
+interface CachedMetadata {
+ statusCode: number;
+ headers: OutgoingHttpHeaders;
+ createdAt: number;
+}
+
+/**
+ * The HTTP proxy implementation.
+ */
+export class HttpCachingProxy {
+ private _options: Required;
+ private _server?: HttpServer;
+
+ /**
+ * URL where the proxy is listening on.
+ * Provide this value to your HTTP client as the proxy configuration.
+ */
+ public url: string;
+
+ constructor(options: ProxyOptions) {
+ this._options = Object.assign({}, DEFAULT_OPTIONS, options);
+ if (!this._options.cachePath) {
+ throw new Error('Required option missing: "cachePath"');
+ }
+ this.url = 'http://proxy-not-running';
+ this._server = undefined;
+ }
+
+ /**
+ * Start listening.
+ */
+ async start() {
+ this._server = createServer(
+ (request: ServerRequest, response: ServerResponse) => {
+ this._handle(request, response);
+ },
+ );
+
+ this._server.on('connect', (req, socket) => {
+ socket.write('HTTP/1.1 501 Not Implemented\r\n\r\n');
+ socket.destroy();
+ });
+
+ this._server.listen(this._options.port);
+ await pEvent(this._server, 'listening');
+
+ const address = this._server.address() as AddressInfo;
+ this.url = `http://127.0.0.1:${address.port}`;
+ }
+
+ /**
+ * Stop listening.
+ */
+ async stop() {
+ if (!this._server) return;
+
+ this.url = 'http://proxy-not-running';
+ const server = this._server;
+ this._server = undefined;
+
+ server.close();
+ await pEvent(server, 'close');
+ }
+
+ private _handle(request: ServerRequest, response: ServerResponse) {
+ try {
+ this._handleAsync(request, response).catch(onerror);
+ } catch (err) {
+ onerror(err);
+ }
+
+ function onerror(error: Error) {
+ console.log('Cannot proxy %s %s.', request.method, request.url, error);
+ response.statusCode = 500;
+ response.end();
+ }
+ }
+
+ private async _handleAsync(request: ServerRequest, response: ServerResponse) {
+ debug(
+ 'Incoming request %s %s',
+ request.method,
+ request.url,
+ request.headers,
+ );
+
+ const cacheKey = this._getCacheKey(request);
+
+ try {
+ const entry = await cacache.get(this._options.cachePath, cacheKey);
+ if (entry.metadata.createdAt + this._options.ttl > Date.now()) {
+ debug('Sending cached response for %s', cacheKey);
+ this._sendCachedEntry(entry.data, entry.metadata, response);
+ return;
+ }
+ debug('Cache entry expired for %s', cacheKey);
+ // (continue to forward the request)
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ console.warn('Cannot load cached entry.', error);
+ }
+ debug('Cache miss for %s', cacheKey);
+ // (continue to forward the request)
+ }
+
+ await this._forwardRequest(request, response);
+ }
+
+ private _getCacheKey(request: ServerRequest): string {
+ // TODO(bajtos) consider adding selected/all headers to the key
+ return `${request.method} ${request.url}`;
+ }
+
+ private _sendCachedEntry(
+ data: Buffer,
+ metadata: CachedMetadata,
+ response: ServerResponse,
+ ) {
+ response.writeHead(metadata.statusCode, metadata.headers);
+ response.end(data);
+ }
+
+ private async _forwardRequest(
+ clientRequest: ServerRequest,
+ clientResponse: ServerResponse,
+ ) {
+ // tslint:disable-next-line:await-promise
+ const backendResponse = await makeRequest({
+ resolveWithFullResponse: true,
+ simple: false,
+
+ method: clientRequest.method,
+ uri: clientRequest.url!,
+ headers: clientRequest.headers,
+ body: clientRequest,
+ });
+
+ debug(
+ 'Got response for %s %s -> %s',
+ clientRequest.method,
+ clientRequest.url,
+ backendResponse.statusCode,
+ backendResponse.headers,
+ );
+
+ const metadata: CachedMetadata = {
+ statusCode: backendResponse.statusCode,
+ headers: backendResponse.headers,
+ createdAt: Date.now(),
+ };
+
+ // Ideally, we should pipe the backend response to both
+ // client response and cachache.put.stream.
+ // r.pipe(clientResponse);
+ // r.pipe(cacache.put.stream(...))
+ // To do so, we would have to defer .end() call on the client
+ // response until the content is stored in the cache,
+ // which is rather complex and involved.
+ // Without that synchronization, the client can start sending
+ // follow-up requests that won't be served from the cache as
+ // the cache has not been updated yet.
+ // Since this proxy is for testing only, buffering the entire
+ // response body is acceptable.
+
+ await cacache.put(
+ this._options.cachePath,
+ this._getCacheKey(clientRequest),
+ backendResponse.body,
+ {metadata},
+ );
+
+ clientResponse.writeHead(
+ backendResponse.statusCode,
+ backendResponse.headers,
+ );
+ clientResponse.end(backendResponse.body);
+ }
+}
diff --git a/packages/http-caching-proxy/src/index.ts b/packages/http-caching-proxy/src/index.ts
new file mode 100644
index 000000000000..eb30e4800e15
--- /dev/null
+++ b/packages/http-caching-proxy/src/index.ts
@@ -0,0 +1,6 @@
+// Copyright IBM Corp. 2018. All Rights Reserved.
+// Node module: @loopback/caching-proxy
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+export * from './http-caching-proxy';
diff --git a/packages/http-caching-proxy/test/integration/http-caching-proxy.integration.ts b/packages/http-caching-proxy/test/integration/http-caching-proxy.integration.ts
new file mode 100644
index 000000000000..91907aaf3e5d
--- /dev/null
+++ b/packages/http-caching-proxy/test/integration/http-caching-proxy.integration.ts
@@ -0,0 +1,221 @@
+// Copyright IBM Corp. 2018. All Rights Reserved.
+// Node module: test-proxy
+// This file is licensed under the MIT License.
+// License text available at https://opensource.org/licenses/MIT
+
+import {expect} from '@loopback/testlab';
+import * as delay from 'delay';
+import * as http from 'http';
+import {AddressInfo} from 'net';
+import * as pEvent from 'p-event';
+import * as path from 'path';
+import * as makeRequest from 'request-promise-native';
+import * as rimrafCb from 'rimraf';
+import * as util from 'util';
+import {HttpCachingProxy, ProxyOptions} from '../../src/http-caching-proxy';
+
+const CACHE_DIR = path.join(__dirname, '.cache');
+
+// tslint:disable:await-promise
+const rimraf = util.promisify(rimrafCb);
+
+describe('TestProxy', () => {
+ let stubServerUrl: string;
+ before(givenStubServer);
+ after(stopStubServer);
+
+ let proxy: HttpCachingProxy;
+ after(stopProxy);
+
+ beforeEach('clean cache dir', async () => await rimraf(CACHE_DIR));
+
+ it('provides "url" property when running', async () => {
+ await givenRunningProxy();
+ expect(proxy.url).to.match(/^http:\/\/127.0.0.1:\d+$/);
+ });
+
+ it('provides invalid "url" property when not running', async () => {
+ proxy = new HttpCachingProxy({cachePath: CACHE_DIR});
+ expect(proxy.url).to.match(/not-running/);
+ });
+
+ it('proxies HTTP requests', async () => {
+ await givenRunningProxy();
+ const result = await makeRequest({
+ uri: 'http://example.com',
+ proxy: proxy.url,
+ resolveWithFullResponse: true,
+ });
+
+ expect(result.statusCode).to.equal(200);
+ expect(result.body).to.containEql('example');
+ });
+
+ it('proxies HTTPs requests (no tunneling)', async () => {
+ await givenRunningProxy();
+ const result = await makeRequest({
+ uri: 'https://example.com',
+ proxy: proxy.url,
+ tunnel: false,
+ resolveWithFullResponse: true,
+ });
+
+ expect(result.statusCode).to.equal(200);
+ expect(result.body).to.containEql('example');
+ });
+
+ it('rejects CONNECT requests (HTTPS tunneling)', async () => {
+ await givenRunningProxy();
+ const resultPromise = makeRequest({
+ uri: 'https://example.com',
+ proxy: proxy.url,
+ tunnel: true,
+ simple: false,
+ resolveWithFullResponse: true,
+ });
+
+ await expect(resultPromise).to.be.rejectedWith(
+ /tunneling socket.*statusCode=501/,
+ );
+ });
+
+ it('forwards request/response headers', async () => {
+ await givenRunningProxy();
+ givenServerDumpsRequests();
+
+ const result = await makeRequest({
+ uri: stubServerUrl,
+ json: true,
+ headers: {'x-client': 'test'},
+ proxy: proxy.url,
+ resolveWithFullResponse: true,
+ });
+
+ expect(result.headers).to.containEql({
+ 'x-server': 'dumping-server',
+ });
+ expect(result.body.headers).to.containDeep({
+ 'x-client': 'test',
+ });
+ });
+
+ it('forwards request body', async () => {
+ await givenRunningProxy();
+ stubServerHandler = (req, res) => req.pipe(res);
+
+ const result = await makeRequest({
+ method: 'POST',
+ uri: stubServerUrl,
+ body: 'a text body',
+ proxy: proxy.url,
+ });
+
+ expect(result).to.equal('a text body');
+ });
+
+ it('caches responses', async () => {
+ await givenRunningProxy();
+ let counter = 1;
+ stubServerHandler = function(req, res) {
+ res.writeHead(201, {'x-counter': counter++});
+ res.end(JSON.stringify({counter: counter++}));
+ };
+
+ const opts = {
+ uri: stubServerUrl,
+ json: true,
+ proxy: proxy.url,
+ resolveWithFullResponse: true,
+ };
+
+ const result1 = await makeRequest(opts);
+ const result2 = await makeRequest(opts);
+
+ expect(result1.statusCode).equal(201);
+
+ expect(result1.statusCode).equal(result2.statusCode);
+ expect(result1.body).deepEqual(result2.body);
+ expect(result1.headers).deepEqual(result2.headers);
+ });
+
+ it('refreshes expired cache entries', async () => {
+ await givenRunningProxy({ttl: 1});
+
+ let counter = 1;
+ stubServerHandler = (req, res) => res.end(String(counter++));
+
+ const opts = {
+ uri: stubServerUrl,
+ proxy: proxy.url,
+ };
+
+ const result1 = await makeRequest(opts);
+ await delay(10);
+ const result2 = await makeRequest(opts);
+
+ expect(result1).to.equal('1');
+ expect(result2).to.equal('2');
+ });
+
+ async function givenRunningProxy(options?: Partial) {
+ proxy = new HttpCachingProxy(
+ Object.assign({cachePath: CACHE_DIR}, options),
+ );
+ await proxy.start();
+ }
+
+ async function stopProxy() {
+ if (!proxy) return;
+ await proxy.stop();
+ }
+
+ let stubServer: http.Server | undefined,
+ stubServerHandler:
+ | ((request: http.ServerRequest, response: http.ServerResponse) => void)
+ | undefined;
+ async function givenStubServer() {
+ stubServerHandler = undefined;
+ stubServer = http.createServer(function handleRequest(req, res) {
+ if (stubServerHandler) {
+ try {
+ stubServerHandler(req, res);
+ } catch (err) {
+ res.end(500);
+ process.nextTick(() => {
+ throw err;
+ });
+ }
+ } else {
+ res.writeHead(501);
+ res.end();
+ }
+ });
+ stubServer.listen(0);
+ await pEvent(stubServer, 'listening');
+ const address = stubServer.address() as AddressInfo;
+ stubServerUrl = `http://127.0.0.1:${address.port}`;
+ }
+
+ async function stopStubServer() {
+ if (!stubServer) return;
+ stubServer.close();
+ await pEvent(stubServer, 'close');
+ stubServer = undefined;
+ }
+
+ function givenServerDumpsRequests() {
+ stubServerHandler = function dumpRequest(req, res) {
+ res.writeHead(200, {
+ 'x-server': 'dumping-server',
+ });
+ res.write(
+ JSON.stringify({
+ method: req.method,
+ url: req.url,
+ headers: req.headers,
+ }),
+ );
+ res.end();
+ };
+ }
+});
diff --git a/packages/http-caching-proxy/tsconfig.build.json b/packages/http-caching-proxy/tsconfig.build.json
new file mode 100644
index 000000000000..3ffcd508d23e
--- /dev/null
+++ b/packages/http-caching-proxy/tsconfig.build.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "http://json.schemastore.org/tsconfig",
+ "extends": "../build/config/tsconfig.common.json",
+ "compilerOptions": {
+ "rootDir": "."
+ },
+ "include": ["index.ts", "src", "test"]
+}