Skip to content

Commit c3414d8

Browse files
committed
Add tests
1 parent 86acf3b commit c3414d8

File tree

9 files changed

+427
-17
lines changed

9 files changed

+427
-17
lines changed

src/logging/formatters.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import prettyBytes from "pretty-bytes";
33

44
import { sizeOf } from "./utils";
55

6-
import type { InternalAxiosRequestConfig } from "axios";
6+
import type { AxiosRequestConfig } from "axios";
77

88
const SENSITIVE_HEADERS = ["Coder-Session-Token", "Proxy-Authorization"];
99

@@ -21,7 +21,7 @@ export function formatTime(ms: number): string {
2121
}
2222

2323
export function formatMethod(method: string | undefined): string {
24-
return (method ?? "GET").toUpperCase();
24+
return (method ? method : "GET").toUpperCase();
2525
}
2626

2727
/**
@@ -55,9 +55,7 @@ export function formatContentLength(
5555
return "(? B)";
5656
}
5757

58-
export function formatUri(
59-
config: InternalAxiosRequestConfig | undefined,
60-
): string {
58+
export function formatUri(config: AxiosRequestConfig | undefined): string {
6159
return config?.url || "<no url>";
6260
}
6361

src/logging/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export function shortId(id: string): string {
55
return id.slice(0, 8);
66
}
77

8+
/**
9+
* Returns the byte size of the data if it can be determined from the data's intrinsic properties,
10+
* otherwise returns undefined (e.g., for plain objects and arrays that would require serialization).
11+
*/
812
export function sizeOf(data: unknown): number | undefined {
913
if (data === null || data === undefined) {
1014
return 0;

src/logging/wsLogger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,6 @@ export class WsLogger {
7777

7878
private formatBytes(): string {
7979
const bytes = prettyBytes(this.byteCount);
80-
return this.unknownByteCount ? `>=${bytes}` : bytes;
80+
return this.unknownByteCount ? `>= ${bytes}` : bytes;
8181
}
8282
}

test/mocks/testHelpers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { vi } from "vitest";
22
import * as vscode from "vscode";
33

4+
import { type Logger } from "@/logging/logger";
5+
46
/**
57
* Mock configuration provider that integrates with the vscode workspace configuration mock.
68
* Use this to set configuration values that will be returned by vscode.workspace.getConfiguration().
@@ -286,3 +288,13 @@ export class InMemorySecretStorage implements vscode.SecretStorage {
286288
this.listeners.forEach((listener) => listener(event));
287289
}
288290
}
291+
292+
export function createMockLogger(): Logger {
293+
return {
294+
trace: vi.fn(),
295+
debug: vi.fn(),
296+
info: vi.fn(),
297+
warn: vi.fn(),
298+
error: vi.fn(),
299+
};
300+
}

test/unit/core/cliManager.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import * as vscode from "vscode";
1212
import { CliManager } from "@/core/cliManager";
1313
import * as cliUtils from "@/core/cliUtils";
1414
import { PathResolver } from "@/core/pathResolver";
15-
import { type Logger } from "@/logging/logger";
1615
import * as pgp from "@/pgp";
1716

1817
import {
18+
createMockLogger,
1919
MockConfigurationProvider,
2020
MockProgressReporter,
2121
MockUserInteraction,
@@ -625,16 +625,6 @@ describe("CliManager", () => {
625625
});
626626
});
627627

628-
function createMockLogger(): Logger {
629-
return {
630-
trace: vi.fn(),
631-
debug: vi.fn(),
632-
info: vi.fn(),
633-
warn: vi.fn(),
634-
error: vi.fn(),
635-
};
636-
}
637-
638628
function createMockApi(version: string, url: string): Api {
639629
const axios = {
640630
defaults: { baseURL: url },
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import {
4+
formatBody,
5+
formatContentLength,
6+
formatHeaders,
7+
formatMethod,
8+
formatTime,
9+
formatUri,
10+
} from "@/logging/formatters";
11+
12+
describe("Logging formatters", () => {
13+
it("formats time in appropriate units", () => {
14+
expect(formatTime(500)).toBe("500ms");
15+
expect(formatTime(1000)).toBe("1.00s");
16+
expect(formatTime(5500)).toBe("5.50s");
17+
expect(formatTime(60000)).toBe("1.00m");
18+
expect(formatTime(150000)).toBe("2.50m");
19+
expect(formatTime(3600000)).toBe("1.00h");
20+
expect(formatTime(7255000)).toBe("2.02h");
21+
});
22+
23+
describe("formatMethod", () => {
24+
it("normalizes HTTP methods to uppercase", () => {
25+
expect(formatMethod("get")).toBe("GET");
26+
expect(formatMethod("post")).toBe("POST");
27+
expect(formatMethod("PUT")).toBe("PUT");
28+
expect(formatMethod("delete")).toBe("DELETE");
29+
});
30+
31+
it("defaults to GET for falsy values", () => {
32+
expect(formatMethod(undefined)).toBe("GET");
33+
expect(formatMethod("")).toBe("GET");
34+
});
35+
});
36+
37+
describe("formatContentLength", () => {
38+
it("uses content-length header when available", () => {
39+
const result = formatContentLength({ "content-length": "1024" }, null);
40+
expect(result).toContain("1.02 kB");
41+
});
42+
43+
it("handles invalid content-length header", () => {
44+
const result = formatContentLength({ "content-length": "invalid" }, null);
45+
expect(result).toContain("?");
46+
});
47+
48+
it.each([
49+
["hello", 5],
50+
[Buffer.from("test"), 4],
51+
[123, 8],
52+
[false, 8],
53+
[BigInt(1234), 4],
54+
[null, 0],
55+
[undefined, 0],
56+
])("calculates size for %s", (data: unknown, bytes: number) => {
57+
const result = formatContentLength({}, data);
58+
expect(result).toContain(`${bytes} B`);
59+
});
60+
61+
it("estimates size for objects", () => {
62+
const result = formatContentLength({}, { foo: "bar" });
63+
expect(result).toMatch(/~\d+/);
64+
});
65+
66+
it("handles circular references safely", () => {
67+
const circular: Record<string, unknown> = { a: 1 };
68+
circular.self = circular;
69+
const result = formatContentLength({}, circular);
70+
expect(result).toMatch(/~\d+/);
71+
});
72+
});
73+
74+
describe("formatUri", () => {
75+
it("returns URL when present", () => {
76+
expect(formatUri({ url: "https://example.com/api" })).toBe(
77+
"https://example.com/api",
78+
);
79+
expect(formatUri({ url: "/relative/path" })).toBe("/relative/path");
80+
});
81+
82+
it("returns placeholder for missing URL", () => {
83+
expect(formatUri(undefined)).toContain("no url");
84+
expect(formatUri({})).toContain("no url");
85+
expect(formatUri({ url: "" })).toContain("no url");
86+
});
87+
});
88+
89+
describe("formatHeaders", () => {
90+
it("formats headers as key-value pairs", () => {
91+
const headers = {
92+
"content-type": "application/json",
93+
accept: "text/html",
94+
};
95+
const result = formatHeaders(headers);
96+
expect(result).toContain("content-type: application/json");
97+
expect(result).toContain("accept: text/html");
98+
});
99+
100+
it("redacts sensitive headers", () => {
101+
const sensitiveHeaders = ["Coder-Session-Token", "Proxy-Authorization"];
102+
103+
sensitiveHeaders.forEach((header) => {
104+
const result = formatHeaders({ [header]: "secret-value" });
105+
expect(result).toContain(`${header}: <redacted>`);
106+
expect(result).not.toContain("secret-value");
107+
});
108+
});
109+
110+
it("returns placeholder for empty headers", () => {
111+
expect(formatHeaders({})).toBe("<no headers>");
112+
});
113+
});
114+
115+
describe("formatBody", () => {
116+
it("formats various body types", () => {
117+
expect(formatBody({ key: "value" })).toContain("key: 'value'");
118+
expect(formatBody("plain text")).toContain("plain text");
119+
expect(formatBody([1, 2, 3])).toContain("1");
120+
expect(formatBody(123)).toContain("123");
121+
expect(formatBody(true)).toContain("true");
122+
});
123+
124+
it("handles circular references gracefully", () => {
125+
const circular: Record<string, unknown> = { a: 1 };
126+
circular.self = circular;
127+
const result = formatBody(circular);
128+
expect(result).toBeTruthy();
129+
expect(result).not.toContain("invalid body");
130+
expect(result).toContain("a: 1");
131+
});
132+
133+
it("handles deep nesting", () => {
134+
const deep = {
135+
level1: { level2: { level3: { level4: { value: "deep" } } } },
136+
};
137+
const result = formatBody(deep);
138+
expect(result).toContain("level4: { value: 'deep' }");
139+
});
140+
141+
it("returns placeholder for empty values", () => {
142+
const emptyValues = [null, undefined, "", 0, false];
143+
emptyValues.forEach((value) => {
144+
expect(formatBody(value)).toContain("no body");
145+
});
146+
});
147+
});
148+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { AxiosError, type AxiosHeaders, type AxiosResponse } from "axios";
2+
import { describe, expect, it, vi } from "vitest";
3+
4+
import {
5+
createRequestMeta,
6+
logError,
7+
logRequest,
8+
logResponse,
9+
} from "@/logging/httpLogger";
10+
import {
11+
HttpClientLogLevel,
12+
type RequestConfigWithMeta,
13+
} from "@/logging/types";
14+
15+
import { createMockLogger } from "../../mocks/testHelpers";
16+
17+
describe("REST HTTP Logger", () => {
18+
describe("log level behavior", () => {
19+
const config = {
20+
method: "POST",
21+
url: "https://api.example.com/endpoint",
22+
headers: {
23+
"content-type": "application/json",
24+
} as unknown as AxiosHeaders,
25+
data: { key: "value" },
26+
metadata: createRequestMeta(),
27+
} as RequestConfigWithMeta;
28+
29+
it("respects NONE level for trace logs", () => {
30+
const logger = createMockLogger();
31+
32+
logRequest(logger, config, HttpClientLogLevel.NONE);
33+
logResponse(
34+
logger,
35+
{ status: 200 } as AxiosResponse,
36+
HttpClientLogLevel.NONE,
37+
);
38+
logError(logger, new Error("test"), HttpClientLogLevel.NONE);
39+
40+
expect(logger.trace).not.toHaveBeenCalled();
41+
expect(logger.error).toHaveBeenCalled(); // always log errors
42+
});
43+
44+
it("includes headers at HEADERS level but not at BASIC", () => {
45+
const logger = createMockLogger();
46+
47+
logRequest(logger, config, HttpClientLogLevel.BASIC);
48+
expect(logger.trace).not.toHaveBeenCalledWith(
49+
expect.stringContaining("content-type"),
50+
);
51+
52+
vi.clearAllMocks();
53+
logRequest(logger, config, HttpClientLogLevel.HEADERS);
54+
expect(logger.trace).toHaveBeenCalledWith(
55+
expect.stringContaining("content-type"),
56+
);
57+
});
58+
59+
it("includes body at BODY level but not at HEADERS", () => {
60+
const logger = createMockLogger();
61+
62+
logRequest(logger, config, HttpClientLogLevel.HEADERS);
63+
expect(logger.trace).not.toHaveBeenCalledWith(
64+
expect.stringContaining("key: 'value'"),
65+
);
66+
67+
vi.clearAllMocks();
68+
logRequest(logger, config, HttpClientLogLevel.BODY);
69+
expect(logger.trace).toHaveBeenCalledWith(
70+
expect.stringContaining("key: 'value'"),
71+
);
72+
});
73+
});
74+
75+
describe("error handling", () => {
76+
it("distinguishes between network errors and response errors", () => {
77+
const logger = createMockLogger();
78+
79+
const networkError = new AxiosError("Some Network Error", "ECONNREFUSED");
80+
networkError.config = {
81+
metadata: createRequestMeta(),
82+
} as RequestConfigWithMeta;
83+
84+
logError(logger, networkError, HttpClientLogLevel.BASIC);
85+
expect(logger.error).toHaveBeenCalledWith(
86+
expect.stringContaining("Some Network Error"),
87+
);
88+
89+
// Response error (4xx/5xx)
90+
vi.clearAllMocks();
91+
const responseError = new AxiosError("Bad Request");
92+
responseError.config = {
93+
metadata: createRequestMeta(),
94+
} as RequestConfigWithMeta;
95+
responseError.response = { status: 400 } as AxiosResponse;
96+
97+
logError(logger, responseError, HttpClientLogLevel.BASIC);
98+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("400"));
99+
expect(logger.error).toHaveBeenCalledWith(
100+
expect.stringContaining("Bad Request"),
101+
);
102+
});
103+
104+
it("handles non-Axios errors", () => {
105+
const logger = createMockLogger();
106+
const error = new Error("Generic error");
107+
108+
logError(logger, error, HttpClientLogLevel.BASIC);
109+
expect(logger.error).toHaveBeenCalledWith("Request error", error);
110+
});
111+
});
112+
});

0 commit comments

Comments
 (0)