Skip to content

Commit bc37828

Browse files
committed
Add tests
1 parent c65c990 commit bc37828

File tree

9 files changed

+458
-17
lines changed

9 files changed

+458
-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().
@@ -264,3 +266,13 @@ export class InMemorySecretStorage implements vscode.SecretStorage {
264266
this.isCorrupted = true;
265267
}
266268
}
269+
270+
export function createMockLogger(): Logger {
271+
return {
272+
trace: vi.fn(),
273+
debug: vi.fn(),
274+
info: vi.fn(),
275+
warn: vi.fn(),
276+
error: vi.fn(),
277+
};
278+
}

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: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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(7200000)).toBe("2.00h");
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 undefined", () => {
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("calculates size for strings", () => {
49+
const result = formatContentLength({}, "hello");
50+
expect(result).toContain("5 B");
51+
});
52+
53+
it("calculates size for buffers", () => {
54+
const result = formatContentLength({}, Buffer.from("test"));
55+
expect(result).toContain("4 B");
56+
});
57+
58+
it("estimates size for objects", () => {
59+
const result = formatContentLength({}, { foo: "bar" });
60+
expect(result).toMatch(/~\d+/);
61+
});
62+
63+
it("handles circular references safely", () => {
64+
const circular: Record<string, unknown> = { a: 1 };
65+
circular.self = circular;
66+
const result = formatContentLength({}, circular);
67+
expect(result).toMatch(/~\d+/);
68+
});
69+
70+
it("handles null and undefined", () => {
71+
expect(formatContentLength({}, null)).toContain("0");
72+
expect(formatContentLength({}, undefined)).toContain("0");
73+
});
74+
});
75+
76+
describe("formatUri", () => {
77+
it("returns URL when present", () => {
78+
expect(formatUri({ url: "https://example.com/api" })).toBe(
79+
"https://example.com/api",
80+
);
81+
expect(formatUri({ url: "/relative/path" })).toBe("/relative/path");
82+
});
83+
84+
it("returns placeholder for missing URL", () => {
85+
expect(formatUri(undefined)).toContain("no url");
86+
expect(formatUri({})).toContain("no url");
87+
expect(formatUri({ url: "" })).toContain("no url");
88+
});
89+
});
90+
91+
describe("formatHeaders", () => {
92+
it("formats headers as key-value pairs", () => {
93+
const headers = {
94+
"content-type": "application/json",
95+
accept: "text/html",
96+
};
97+
const result = formatHeaders(headers);
98+
expect(result).toContain("content-type: application/json");
99+
expect(result).toContain("accept: text/html");
100+
});
101+
102+
it("redacts sensitive headers", () => {
103+
const sensitiveHeaders = ["Coder-Session-Token", "Proxy-Authorization"];
104+
105+
sensitiveHeaders.forEach((header) => {
106+
const result = formatHeaders({ [header]: "secret-value" });
107+
expect(result).toContain(`${header}: <redacted>`);
108+
expect(result).not.toContain("secret-value");
109+
});
110+
});
111+
112+
it("returns placeholder for empty headers", () => {
113+
expect(formatHeaders({})).toBe("<no headers>");
114+
});
115+
});
116+
117+
describe("formatBody", () => {
118+
it("formats various body types", () => {
119+
expect(formatBody({ key: "value" })).toContain("key: 'value'");
120+
expect(formatBody("plain text")).toContain("plain text");
121+
expect(formatBody([1, 2, 3])).toContain("1");
122+
expect(formatBody(123)).toContain("123");
123+
expect(formatBody(true)).toContain("true");
124+
});
125+
126+
it("handles circular references gracefully", () => {
127+
const circular: Record<string, unknown> = { a: 1 };
128+
circular.self = circular;
129+
const result = formatBody(circular);
130+
expect(result).toBeTruthy();
131+
expect(result).not.toContain("invalid body");
132+
expect(result).toContain("a: 1");
133+
});
134+
135+
it("handles deep nesting", () => {
136+
const deep = {
137+
level1: { level2: { level3: { level4: { value: "deep" } } } },
138+
};
139+
const result = formatBody(deep);
140+
expect(result).toContain("level4: { value: 'deep' }");
141+
});
142+
143+
it("returns placeholder for empty values", () => {
144+
const emptyValues = [null, undefined, "", 0, false];
145+
emptyValues.forEach((value) => {
146+
expect(formatBody(value)).toContain("no body");
147+
});
148+
});
149+
});
150+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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("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("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+
});
100+
101+
it("handles non-Axios errors", () => {
102+
const logger = createMockLogger();
103+
const error = new Error("Generic error");
104+
105+
logError(logger, error, HttpClientLogLevel.BASIC);
106+
expect(logger.error).toHaveBeenCalledWith("Request error", error);
107+
});
108+
});
109+
});

0 commit comments

Comments
 (0)