Repository: kubernetes-client/javascript
Pull request #2272 (2025-02-25 21:56:28)

Improve coverage by adding unit tests for log.ts
Largely generated by github copilot (ftw!)

Found a genuine bug where the fetch `response.body` wasn't being read as a stream.

Add an exclusion for some generated code that we probably don't want to test.


Patch:
diff --git a/package.json b/package.json
index aff86a68..9a29b0d1 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
             "src/**/*.ts"
         ],
         "exclude": [
-            "src/gen/*/**.ts",
+            "src/gen/**/*.ts",
             "src/index.ts",
             "src/*_test.ts",
             "src/test"
diff --git a/src/log.ts b/src/log.ts
index e209c3da..419c8b9a 100644
--- a/src/log.ts
+++ b/src/log.ts
@@ -142,7 +142,7 @@ export class Log {
                 // TODO: the follow search param still has the stream close prematurely based on my testing
                 response.body!.pipe(stream);
             } else if (status === 500) {
-                const v1status = response.body as V1Status;
+                const v1status = (await response.json()) as V1Status;
                 const v1code = v1status.code;
                 const v1message = v1status.message;
                 if (v1code !== undefined && v1message !== undefined) {
@@ -152,6 +152,13 @@ export class Log {
                         v1status,
                         normalizeResponseHeaders(response),
                     );
+                } else {
+                    throw new ApiException<undefined>(
+                        status,
+                        'Error occurred in log request',
+                        undefined,
+                        normalizeResponseHeaders(response),
+                    );
                 }
             } else {
                 throw new ApiException<undefined>(


Test patch:
diff --git a/src/log_test.ts b/src/log_test.ts
index ef5862aa..20f0de22 100644
--- a/src/log_test.ts
+++ b/src/log_test.ts
@@ -1,8 +1,177 @@
-import { strictEqual, throws } from 'node:assert';
-import { AddOptionsToSearchParams, LogOptions } from './log.js';
+import { strictEqual, rejects, throws } from 'node:assert';
+import nock from 'nock';
+import { AddOptionsToSearchParams, Log, LogOptions } from './log.js';
+import { KubeConfig } from './config.js';
+import { Writable } from 'node:stream';
 
 describe('Log', () => {
+    describe('Constructor', () => {
+        it('should work', () => {
+            const config = new KubeConfig();
+            config.addCluster({
+                name: 'foo',
+                server: 'https://example.com',
+                caData: 'certificate-authority-data',
+                skipTLSVerify: false,
+            });
+            const log = new Log(config);
+            strictEqual(log.config, config);
+        });
+    });
+    describe('log', () => {
+        const config = new KubeConfig();
+        config.addCluster({
+            name: 'foo',
+            server: 'https://example.com',
+            caData: 'certificate-authority-data',
+            skipTLSVerify: false,
+        });
+        config.addContext({
+            name: 'foo',
+            cluster: 'foo',
+            user: 'foo',
+        });
+        config.setCurrentContext('foo');
+        const log = new Log(config);
+
+        afterEach(() => {
+            nock.cleanAll();
+        });
+
+        it('should make a request with correct parameters', async () => {
+            const namespace = 'default';
+            const podName = 'mypod';
+            const containerName = 'mycontainer';
+            const stream = new Writable({
+                write(chunk, encoding, callback) {
+                    callback();
+                },
+            });
+            const options: LogOptions = {
+                follow: true,
+                limitBytes: 100,
+                pretty: true,
+                previous: true,
+                sinceSeconds: 1,
+                tailLines: 1,
+                timestamps: true,
+            };
+
+            nock('https://example.com')
+                .get('/api/v1/namespaces/default/pods/mypod/log')
+                .query({
+                    container: 'mycontainer',
+                    follow: 'true',
+                    limitBytes: '100',
+                    pretty: 'true',
+                    previous: 'true',
+                    sinceSeconds: '1',
+                    tailLines: '1',
+                    timestamps: 'true',
+                })
+                .reply(200, 'log data');
+
+            const controller = await log.log(namespace, podName, containerName, stream, options);
+            strictEqual(controller instanceof AbortController, true);
+        });
+
+        it('should throw an error if no active cluster', async () => {
+            const configWithoutCluster = new KubeConfig();
+            const logWithoutCluster = new Log(configWithoutCluster);
+            const namespace = 'default';
+            const podName = 'mypod';
+            const containerName = 'mycontainer';
+            const stream = new Writable({
+                write(chunk, encoding, callback) {
+                    callback();
+                },
+            });
+
+            await rejects(async () => {
+                await logWithoutCluster.log(namespace, podName, containerName, stream);
+            }, /No currently active cluster/);
+        });
+
+        it('should handle API exceptions on non-500', async () => {
+            const namespace = 'default';
+            const podName = 'mypod';
+            const containerName = 'mycontainer';
+            const stream = new Writable({
+                write(chunk, encoding, callback) {
+                    callback();
+                },
+            });
+
+            nock('https://example.com')
+                .get('/api/v1/namespaces/default/pods/mypod/log')
+                .query({ container: 'mycontainer' })
+                .reply(501, { message: 'Error occurred in log request' });
+
+            await rejects(async () => {
+                await log.log(namespace, podName, containerName, stream);
+            }, /Error occurred in log request/);
+        });
+
+        it('should handle API exceptions on 500', async () => {
+            const namespace = 'default';
+            const podName = 'mypod';
+            const containerName = 'mycontainer';
+            const stream = new Writable({
+                write(chunk, encoding, callback) {
+                    callback();
+                },
+            });
+
+            nock('https://example.com')
+                .get('/api/v1/namespaces/default/pods/mypod/log')
+                .query({ container: 'mycontainer' })
+                .reply(500, { message: 'Error occurred in log request' });
+
+            await rejects(async () => {
+                await log.log(namespace, podName, containerName, stream);
+            }, /Error occurred in log request/);
+        });
+
+        it('should handle V1Status with code and message', async () => {
+            const namespace = 'default';
+            const podName = 'mypod';
+            const containerName = 'mycontainer';
+            const stream = new Writable({
+                write(chunk, encoding, callback) {
+                    callback();
+                },
+            });
+
+            const v1Status = {
+                kind: 'Status',
+                apiVersion: 'v1',
+                metadata: {},
+                status: 'Failure',
+                message: 'Pod not found',
+                reason: 'NotFound',
+                details: {
+                    name: podName,
+                    kind: 'pods',
+                },
+                code: 404,
+            };
+
+            nock('https://example.com')
+                .get('/api/v1/namespaces/default/pods/mypod/log')
+                .query({ container: 'mycontainer' })
+                .reply(500, v1Status);
+
+            await rejects(async () => {
+                await log.log(namespace, podName, containerName, stream);
+            }, /Pod not found/);
+        });
+    });
     describe('AddOptionsToSearchParams', () => {
+        it('should handle no log options', () => {
+            const searchParams = new URLSearchParams();
+            AddOptionsToSearchParams(undefined, searchParams);
+            // verify that it doesn't throw.
+        });
         it('should add options to search params', () => {
             let searchParams = new URLSearchParams();
             let options: LogOptions = {