diff --git a/package-lock.json b/package-lock.json index f123df8c4c..a04cf57782 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "eslint-plugin-erasable-syntax-only": "^0.3.0", "husky": "^9.0.6", "mock-fs": "^5.2.0", - "nock": "^13.2.9", + "nock": "^14.0.5", "prettier": "^3.0.0", "pretty-quick": "^4.0.0", "ts-mockito": "^2.3.1", @@ -828,6 +828,24 @@ "jsep": "^0.4.0||^1.0.0" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.38.7", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz", + "integrity": "sha512-Jkb27iSn7JPdkqlTqKfhncFfnEZsIJVYxsFbUSWEkxdIPdsyngrhoDBk0/BGD2FQcRH99vlRrkHpNTyKqI+0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -866,6 +884,31 @@ "node": ">= 8" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2534,6 +2577,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2920,18 +2970,18 @@ "license": "MIT" }, "node_modules/nock": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", - "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.5.tgz", + "integrity": "sha512-R49fALR9caB6vxuSWUIaK2eBYeTloZQUFBZ4rHO+TbhMGQHtwnhdqKLYki+o+8qMgLvoBYWrp/2KzGPhxL4S6w==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.1.0", + "@mswjs/interceptors": "^0.38.7", "json-stringify-safe": "^5.0.1", "propagate": "^2.0.0" }, "engines": { - "node": ">= 10.13" + "node": ">=18.20.0 <20 || >=20.12.1" } }, "node_modules/node-fetch": { @@ -3003,6 +3053,13 @@ "node": ">= 0.8.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3443,6 +3500,13 @@ "bare-events": "^2.2.0" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", diff --git a/package.json b/package.json index 36e5747468..e06d586a8a 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "eslint-plugin-erasable-syntax-only": "^0.3.0", "husky": "^9.0.6", "mock-fs": "^5.2.0", - "nock": "^13.2.9", + "nock": "^14.0.5", "prettier": "^3.0.0", "pretty-quick": "^4.0.0", "ts-mockito": "^2.3.1", diff --git a/src/watch_test.ts b/src/watch_test.ts index 58252b1290..5bf842565d 100644 --- a/src/watch_test.ts +++ b/src/watch_test.ts @@ -5,7 +5,7 @@ import { PassThrough } from 'node:stream'; import { KubeConfig } from './config.js'; import { Cluster, Context, User } from './config_types.js'; import { Watch } from './watch.js'; -import { IncomingMessage, createServer } from 'node:http'; +import { ServerResponse, createServer } from 'node:http'; import { AddressInfo } from 'node:net'; const server = 'https://foo.company.com'; @@ -72,11 +72,7 @@ describe('Watch', () => { s.done(); }); - it('should not call watch done callback more than once', async () => { - const kc = new KubeConfig(); - Object.assign(kc, fakeConfig); - const watch = new Watch(kc); - + it('should not call watch done callback more than once', async (t) => { const obj1 = { type: 'ADDED', object: { @@ -93,26 +89,13 @@ describe('Watch', () => { const path = '/some/path/to/object'; - const stream = new PassThrough(); - - const [scope] = systemUnderTest(); - - let response: IncomingMessage | undefined; - - const s = scope - .get(path) - .query({ - watch: 'true', - a: 'b', - }) - .reply(200, function (): PassThrough { - this.req.on('response', (r) => { - response = r; - }); - stream.push(JSON.stringify(obj1) + '\n'); - stream.push(JSON.stringify(obj2) + '\n'); - return stream; - }); + let response: ServerResponse | undefined; + const kc = await setupMockSystem(t, (req, res) => { + response = res; + res.write(JSON.stringify(obj1) + '\n'); + res.write(JSON.stringify(obj2) + '\n'); + }); + const watch = new Watch(kc); const receivedTypes: string[] = []; const receivedObjects: string[] = []; @@ -154,57 +137,27 @@ describe('Watch', () => { deepStrictEqual(receivedObjects, [obj1.object, obj2.object]); strictEqual(doneCalled, 0); - - const errIn = new Error('err'); - (response as IncomingMessage).destroy(errIn); - + response!.destroy(); await donePromise; - strictEqual(doneCalled, 1); - deepStrictEqual(doneErr, errIn); - - s.done(); - - stream.destroy(); + strictEqual(doneErr.code, 'ERR_STREAM_PREMATURE_CLOSE'); }); - it('should not call the done callback more than once on unexpected connection loss', async () => { + it('should not call the done callback more than once on unexpected connection loss', async (t) => { // Create a server that accepts the connection and flushes headers, then // immediately destroys the connection (causing a "Premature close" // error). // // This reproduces a bug where AbortController.abort() inside // doneCallOnce could cause done() to be invoked twice. - - const mockServer = createServer((req, res) => { + const kc = await setupMockSystem(t, (req, res) => { res.writeHead(200, { 'Content-Type': 'application/json', 'Transfer-Encoding': 'chunked', }); - res.flushHeaders(); res.destroy(); // Prematurely close the connection }); - - const mockServerPort = await new Promise((resolve) => { - mockServer.listen(0, () => { - resolve((mockServer.address() as AddressInfo).port); - }); - }); - - const kc = new KubeConfig(); - - kc.loadFromClusterAndUser( - { - name: 'cluster', - server: `http://localhost:${mockServerPort}`, - skipTLSVerify: true, - }, - { - name: 'user', - }, - ); - const watch = new Watch(kc); let doneCalled = 0; @@ -225,15 +178,15 @@ describe('Watch', () => { ); await donePromise; - - mockServer.close(); - strictEqual(doneCalled, 1); }); - it('should call setKeepAlive on the socket to extend the default of 5 mins', async () => { - const kc = new KubeConfig(); - + it('should call setKeepAlive on the socket to extend the default of 5 mins', async (t) => { + let response: ServerResponse | undefined; + const kc = await setupMockSystem(t, (req, res) => { + response = res; + res.write(JSON.stringify(obj1) + '\n'); + }); const mockSocket = { setKeepAlive: function (enable: boolean, timeout: number) { this.keepAliveEnabled = enable; @@ -242,16 +195,16 @@ describe('Watch', () => { keepAliveEnabled: false, keepAliveTimeout: 0, }; - Object.assign(kc, { - ...fakeConfig, - applyToFetchOptions: async () => ({ + + (kc as any).applyToFetchOptions = async () => { + return { agent: { sockets: { 'mock-url': [mockSocket], }, }, - }), - }); + }; + }; const watch = new Watch(kc); const obj1 = { @@ -262,27 +215,6 @@ describe('Watch', () => { }; const path = '/some/path/to/object'; - - const stream = new PassThrough(); - - const [scope] = systemUnderTest(); - - let response: IncomingMessage | undefined; - - const s = scope - .get(path) - .query({ - watch: 'true', - a: 'b', - }) - .reply(200, function (): PassThrough { - this.req.on('response', (r) => { - response = r; - }); - stream.push(JSON.stringify(obj1) + '\n'); - return stream; - }); - const receivedTypes: string[] = []; const receivedObjects: string[] = []; let doneCalled = 0; @@ -326,46 +258,28 @@ describe('Watch', () => { strictEqual(mockSocket.keepAliveEnabled, true); strictEqual(mockSocket.keepAliveTimeout, 30000); - const errIn = new Error('err'); - (response as IncomingMessage).destroy(errIn); + response!.destroy(); await donePromise; strictEqual(doneCalled, 1); - deepStrictEqual(doneErr, errIn); - - s.done(); - - stream.destroy(); + strictEqual(doneErr.code, 'ERR_STREAM_PREMATURE_CLOSE'); }); - it('should handle server errors correctly', async () => { - const kc = new KubeConfig(); - Object.assign(kc, fakeConfig); - const watch = new Watch(kc); - + it('should handle server errors correctly', async (t) => { const obj1 = { type: 'ADDED', object: { foo: 'bar', }, }; - - const stream = new PassThrough(); - - const [scope] = systemUnderTest(); - const path = '/some/path/to/object?watch=true'; - - let response: IncomingMessage | undefined; - - const s = scope.get(path).reply(200, function (): PassThrough { - this.req.on('response', (r) => { - response = r; - }); - stream.push(JSON.stringify(obj1) + '\n'); - return stream; + let response: ServerResponse | undefined; + const kc = await setupMockSystem(t, (req, res) => { + response = res; + res.write(JSON.stringify(obj1) + '\n'); }); + const watch = new Watch(kc); const receivedTypes: string[] = []; const receivedObjects: string[] = []; @@ -405,16 +319,12 @@ describe('Watch', () => { strictEqual(doneErr.length, 0); const errIn = new Error('err'); - (response as IncomingMessage).destroy(errIn); + response!.destroy(errIn); await donePromise; strictEqual(doneErr.length, 1); - deepStrictEqual(doneErr[0], errIn); - - s.done(); - - stream.destroy(); + strictEqual(doneErr[0].code, 'ERR_STREAM_PREMATURE_CLOSE'); }); it('should handle server side close correctly', async () => { @@ -555,3 +465,31 @@ describe('Watch', () => { await rejects(promise); }); }); + +async function setupMockSystem(ctx, handler) { + const server = createServer(handler); + ctx.after(() => { + try { + server.close(); + } catch { + // Ignore errors during server close. + } + }); + const port = await new Promise((resolve) => { + server.listen(0, () => { + resolve((server.address() as AddressInfo).port); + }); + }); + const kc = new KubeConfig(); + kc.loadFromClusterAndUser( + { + name: 'cluster', + server: `http://localhost:${port}`, + skipTLSVerify: true, + }, + { + name: 'user', + }, + ); + return kc; +}