Skip to content

Commit 0d4bd15

Browse files
committed
Address test coverage
1 parent 354677c commit 0d4bd15

File tree

5 files changed

+238
-3
lines changed

5 files changed

+238
-3
lines changed

src/node/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ export const options: Options<Required<UserProvidedArgs>> = {
300300
},
301301
i18n: {
302302
type: "string",
303+
path: true,
303304
description: "Path to JSON file with custom translations. Merges with default strings and supports all i18n keys.",
304305
},
305306
}

src/node/i18n/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import i18next, { init } from "i18next"
21
import { promises as fs } from "fs"
2+
import i18next, { init } from "i18next"
33
import * as en from "./locales/en.json"
44
import * as ja from "./locales/ja.json"
55
import * as th from "./locales/th.json"

test/unit/node/cli.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe("parser", () => {
7575
"--verbose",
7676
["--app-name", "custom instance name"],
7777
["--welcome-text", "welcome to code"],
78-
["--i18n", '{"LOGIN_TITLE": "Custom Portal"}'],
78+
["--i18n", "path/to/custom-strings.json"],
7979
"2",
8080

8181
["--locale", "ja"],
@@ -146,7 +146,7 @@ describe("parser", () => {
146146
verbose: true,
147147
"app-name": "custom instance name",
148148
"welcome-text": "welcome to code",
149-
i18n: '{"LOGIN_TITLE": "Custom Portal"}',
149+
i18n: path.resolve("path/to/custom-strings.json"),
150150
version: true,
151151
"bind-addr": "192.169.0.1:8080",
152152
"session-socket": "/tmp/override-code-server-ipc-socket",

test/unit/node/i18n.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,64 @@ describe("i18n", () => {
9191

9292
await expect(loadCustomStrings(unicodeJsonFile)).resolves.toBeUndefined()
9393
})
94+
95+
it("should handle generic errors that are not ENOENT or SyntaxError", async () => {
96+
const testFile = path.join(tempDir, "test.json")
97+
await fs.writeFile(testFile, "{}")
98+
99+
// Mock fs.readFile to throw a generic error
100+
const originalReadFile = fs.readFile
101+
const mockError = new Error("Permission denied")
102+
fs.readFile = jest.fn().mockRejectedValue(mockError)
103+
104+
await expect(loadCustomStrings(testFile)).rejects.toThrow(
105+
`Failed to load custom strings from ${testFile}: Permission denied`,
106+
)
107+
108+
// Restore original function
109+
fs.readFile = originalReadFile
110+
})
111+
112+
it("should handle errors that are not Error instances", async () => {
113+
const testFile = path.join(tempDir, "test.json")
114+
await fs.writeFile(testFile, "{}")
115+
116+
// Mock fs.readFile to throw a non-Error object
117+
const originalReadFile = fs.readFile
118+
fs.readFile = jest.fn().mockRejectedValue("String error")
119+
120+
await expect(loadCustomStrings(testFile)).rejects.toThrow(
121+
`Failed to load custom strings from ${testFile}: String error`,
122+
)
123+
124+
// Restore original function
125+
fs.readFile = originalReadFile
126+
})
127+
128+
it("should handle null/undefined errors", async () => {
129+
const testFile = path.join(tempDir, "test.json")
130+
await fs.writeFile(testFile, "{}")
131+
132+
// Mock fs.readFile to throw null
133+
const originalReadFile = fs.readFile
134+
fs.readFile = jest.fn().mockRejectedValue(null)
135+
136+
await expect(loadCustomStrings(testFile)).rejects.toThrow(`Failed to load custom strings from ${testFile}: null`)
137+
138+
// Restore original function
139+
fs.readFile = originalReadFile
140+
})
141+
142+
it("should complete without errors for valid input", async () => {
143+
const testFile = path.join(tempDir, "resource-test.json")
144+
const customStrings = {
145+
WELCOME: "Custom Welcome Message",
146+
LOGIN_TITLE: "Custom Login Title",
147+
}
148+
await fs.writeFile(testFile, JSON.stringify(customStrings))
149+
150+
// Should not throw any errors
151+
await expect(loadCustomStrings(testFile)).resolves.toBeUndefined()
152+
})
94153
})
95154
})

test/unit/node/main.test.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { promises as fs } from "fs"
2+
import * as path from "path"
3+
import { setDefaults, parse } from "../../../src/node/cli"
4+
import { loadCustomStrings } from "../../../src/node/i18n"
5+
import { tmpdir } from "../../utils/helpers"
6+
7+
// Mock the i18n module
8+
jest.mock("../../../src/node/i18n", () => ({
9+
loadCustomStrings: jest.fn(),
10+
}))
11+
12+
// Mock logger to avoid console output during tests
13+
jest.mock("@coder/logger", () => ({
14+
logger: {
15+
info: jest.fn(),
16+
debug: jest.fn(),
17+
warn: jest.fn(),
18+
error: jest.fn(),
19+
level: 0,
20+
},
21+
field: jest.fn(),
22+
Level: {
23+
Trace: 0,
24+
Debug: 1,
25+
Info: 2,
26+
Warn: 3,
27+
Error: 4,
28+
},
29+
}))
30+
31+
const mockedLoadCustomStrings = loadCustomStrings as jest.MockedFunction<typeof loadCustomStrings>
32+
33+
describe("main", () => {
34+
let tempDir: string
35+
let mockServer: any
36+
37+
beforeEach(async () => {
38+
tempDir = await tmpdir("code-server-main-test")
39+
40+
// Reset mocks
41+
jest.clearAllMocks()
42+
43+
// Mock the server creation to avoid actually starting a server
44+
mockServer = {
45+
server: {
46+
listen: jest.fn(),
47+
address: jest.fn(() => ({ address: "127.0.0.1", port: 8080 })),
48+
close: jest.fn(),
49+
},
50+
editorSessionManagerServer: {
51+
address: jest.fn(() => null),
52+
},
53+
dispose: jest.fn(),
54+
}
55+
})
56+
57+
afterEach(async () => {
58+
// Clean up temp directory
59+
try {
60+
await fs.rmdir(tempDir, { recursive: true })
61+
} catch (error) {
62+
// Ignore cleanup errors
63+
}
64+
})
65+
66+
describe("runCodeServer", () => {
67+
it("should load custom strings when i18n flag is provided", async () => {
68+
// Create a test custom strings file
69+
const customStringsFile = path.join(tempDir, "custom-strings.json")
70+
await fs.writeFile(
71+
customStringsFile,
72+
JSON.stringify({
73+
WELCOME: "Custom Welcome",
74+
LOGIN_TITLE: "My App",
75+
}),
76+
)
77+
78+
// Create args with i18n flag
79+
const cliArgs = parse([
80+
`--config=${path.join(tempDir, "config.yaml")}`,
81+
`--user-data-dir=${tempDir}`,
82+
"--bind-addr=localhost:0",
83+
"--log=warn",
84+
"--auth=none",
85+
`--i18n=${customStringsFile}`,
86+
])
87+
const args = await setDefaults(cliArgs)
88+
89+
// Mock the app module
90+
jest.doMock("../../../src/node/app", () => ({
91+
createApp: jest.fn().mockResolvedValue(mockServer),
92+
ensureAddress: jest.fn().mockReturnValue(new URL("http://localhost:8080")),
93+
}))
94+
95+
// Mock routes module
96+
jest.doMock("../../../src/node/routes", () => ({
97+
register: jest.fn().mockResolvedValue(jest.fn()),
98+
}))
99+
100+
// Mock loadCustomStrings to succeed
101+
mockedLoadCustomStrings.mockResolvedValue(undefined)
102+
103+
// Import runCodeServer after mocking
104+
const mainModule = await import("../../../src/node/main")
105+
const result = await mainModule.runCodeServer(args)
106+
107+
// Verify that loadCustomStrings was called with the correct file path
108+
expect(mockedLoadCustomStrings).toHaveBeenCalledWith(customStringsFile)
109+
expect(mockedLoadCustomStrings).toHaveBeenCalledTimes(1)
110+
111+
// Clean up
112+
await result.dispose()
113+
})
114+
115+
it("should not load custom strings when i18n flag is not provided", async () => {
116+
// Create args without i18n flag
117+
const cliArgs = parse([
118+
`--config=${path.join(tempDir, "config.yaml")}`,
119+
`--user-data-dir=${tempDir}`,
120+
"--bind-addr=localhost:0",
121+
"--log=warn",
122+
"--auth=none",
123+
])
124+
const args = await setDefaults(cliArgs)
125+
126+
// Mock the app module
127+
jest.doMock("../../../src/node/app", () => ({
128+
createApp: jest.fn().mockResolvedValue(mockServer),
129+
ensureAddress: jest.fn().mockReturnValue(new URL("http://localhost:8080")),
130+
}))
131+
132+
// Mock routes module
133+
jest.doMock("../../../src/node/routes", () => ({
134+
register: jest.fn().mockResolvedValue(jest.fn()),
135+
}))
136+
137+
// Import runCodeServer after mocking
138+
const mainModule = await import("../../../src/node/main")
139+
const result = await mainModule.runCodeServer(args)
140+
141+
// Verify that loadCustomStrings was NOT called
142+
expect(mockedLoadCustomStrings).not.toHaveBeenCalled()
143+
144+
// Clean up
145+
await result.dispose()
146+
})
147+
148+
it("should handle errors when loadCustomStrings fails", async () => {
149+
// Create args with i18n flag pointing to non-existent file
150+
const nonExistentFile = path.join(tempDir, "does-not-exist.json")
151+
const cliArgs = parse([
152+
`--config=${path.join(tempDir, "config.yaml")}`,
153+
`--user-data-dir=${tempDir}`,
154+
"--bind-addr=localhost:0",
155+
"--log=warn",
156+
"--auth=none",
157+
`--i18n=${nonExistentFile}`,
158+
])
159+
const args = await setDefaults(cliArgs)
160+
161+
// Mock loadCustomStrings to throw an error
162+
const mockError = new Error("Custom strings file not found")
163+
mockedLoadCustomStrings.mockRejectedValue(mockError)
164+
165+
// Import runCodeServer after mocking
166+
const mainModule = await import("../../../src/node/main")
167+
168+
// Verify that runCodeServer throws the error from loadCustomStrings
169+
await expect(mainModule.runCodeServer(args)).rejects.toThrow("Custom strings file not found")
170+
171+
// Verify that loadCustomStrings was called
172+
expect(mockedLoadCustomStrings).toHaveBeenCalledWith(nonExistentFile)
173+
})
174+
})
175+
})

0 commit comments

Comments
 (0)