Skip to content

Commit 8f04fac

Browse files
authored
Merge branch 'main' into renovate/azure-setup-helm-2.x
2 parents 054ee59 + e3c8bd6 commit 8f04fac

File tree

7 files changed

+155
-17
lines changed

7 files changed

+155
-17
lines changed

src/node/app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ export const ensureAddress = (server: http.Server, protocol: string): URL | stri
9494
}
9595

9696
if (typeof addr !== "string") {
97-
return new URL(`${protocol}://${addr.address}:${addr.port}`)
97+
const host = addr.family === "IPv6" ? `[${addr.address}]` : addr.address
98+
return new URL(`${protocol}://${host}:${addr.port}`)
9899
}
99100

100101
// If this is a string then it is a pipe or Unix socket.

src/node/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
542542
args.password = process.env.PASSWORD
543543
}
544544

545-
if (process.env.CS_DISABLE_FILE_DOWNLOADS === "1") {
545+
if (process.env.CS_DISABLE_FILE_DOWNLOADS?.match(/^(1|true)$/)) {
546546
args["disable-file-downloads"] = true
547547
}
548548

src/node/heart.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,7 @@ export class Heart {
3333
if (typeof this.heartbeatTimer !== "undefined") {
3434
clearTimeout(this.heartbeatTimer)
3535
}
36-
this.heartbeatTimer = setTimeout(() => {
37-
this.isActive()
38-
.then((active) => {
39-
if (active) {
40-
this.beat()
41-
}
42-
})
43-
.catch((error) => {
44-
logger.warn(error.message)
45-
})
46-
}, this.heartbeatInterval)
36+
this.heartbeatTimer = setTimeout(() => heartbeatTimer(this.isActive, this.beat), this.heartbeatInterval)
4737
}
4838

4939
/**
@@ -55,3 +45,20 @@ export class Heart {
5545
}
5646
}
5747
}
48+
49+
/**
50+
* Helper function for the heartbeatTimer.
51+
*
52+
* If heartbeat is active, call beat. Otherwise do nothing.
53+
*
54+
* Extracted to make it easier to test.
55+
*/
56+
export async function heartbeatTimer(isActive: Heart["isActive"], beat: Heart["beat"]) {
57+
try {
58+
if (await isActive()) {
59+
beat()
60+
}
61+
} catch (error: unknown) {
62+
logger.warn((error as Error).message)
63+
}
64+
}

test/e2e/downloads.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import * as path from "path"
21
import { promises as fs } from "fs"
2+
import * as path from "path"
33
import { clean } from "../utils/helpers"
44
import { describe, test, expect } from "./baseFixture"
55

test/unit/node/app.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,20 @@ describe("ensureAddress", () => {
152152
it("should throw and error if no address", () => {
153153
expect(() => ensureAddress(mockServer, "http")).toThrow("Server has no address")
154154
})
155-
it("should return the address if it exists", async () => {
156-
mockServer.address = () => "http://localhost:8080/"
155+
it("should return the address if it's a string", async () => {
156+
mockServer.address = () => "/path/to/unix.sock"
157157
const address = ensureAddress(mockServer, "http")
158-
expect(address.toString()).toBe(`http://localhost:8080/`)
158+
expect(address.toString()).toBe(`/path/to/unix.sock`)
159+
})
160+
it("should construct URL with an IPv4 address", async () => {
161+
mockServer.address = () => ({ address: "1.2.3.4", port: 5678, family: "IPv4" })
162+
const address = ensureAddress(mockServer, "http")
163+
expect(address.toString()).toBe(`http://1.2.3.4:5678/`)
164+
})
165+
it("should construct URL with an IPv6 address", async () => {
166+
mockServer.address = () => ({ address: "a:b:c:d::1234", port: 5678, family: "IPv6" })
167+
const address = ensureAddress(mockServer, "http")
168+
expect(address.toString()).toBe(`http://[a:b:c:d::1234]:5678/`)
159169
})
160170
})
161171

test/unit/node/cli.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,18 @@ describe("parser", () => {
362362
})
363363
})
364364

365+
it("should use env var CS_DISABLE_FILE_DOWNLOADS set to true", async () => {
366+
process.env.CS_DISABLE_FILE_DOWNLOADS = "true"
367+
const args = parse([])
368+
expect(args).toEqual({})
369+
370+
const defaultArgs = await setDefaults(args)
371+
expect(defaultArgs).toEqual({
372+
...defaults,
373+
"disable-file-downloads": true,
374+
})
375+
})
376+
365377
it("should error if password passed in", () => {
366378
expect(() => parse(["--password", "supersecret123"])).toThrowError(
367379
"--password can only be set in the config file or passed in via $PASSWORD",

test/unit/node/heart.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { logger } from "@coder/logger"
2+
import { readFile, writeFile, stat, utimes } from "fs/promises"
3+
import { Heart, heartbeatTimer } from "../../../src/node/heart"
4+
import { clean, mockLogger, tmpdir } from "../../utils/helpers"
5+
6+
const mockIsActive = (resolveTo: boolean) => jest.fn().mockResolvedValue(resolveTo)
7+
8+
describe("Heart", () => {
9+
const testName = "heartTests"
10+
let testDir = ""
11+
let heart: Heart
12+
13+
beforeAll(async () => {
14+
mockLogger()
15+
await clean(testName)
16+
testDir = await tmpdir(testName)
17+
})
18+
beforeEach(() => {
19+
heart = new Heart(`${testDir}/shutdown.txt`, mockIsActive(true))
20+
})
21+
afterAll(() => {
22+
jest.restoreAllMocks()
23+
})
24+
afterEach(() => {
25+
jest.resetAllMocks()
26+
if (heart) {
27+
heart.dispose()
28+
}
29+
})
30+
it("should write to a file when given a valid file path", async () => {
31+
// Set up heartbeat file with contents
32+
const text = "test"
33+
const pathToFile = `${testDir}/file.txt`
34+
await writeFile(pathToFile, text)
35+
const fileContents = await readFile(pathToFile, { encoding: "utf8" })
36+
// Explicitly set the modified time to 0 so that we can check
37+
// that the file was indeed modified after calling heart.beat().
38+
// This works around any potential race conditions.
39+
// Docs: https://nodejs.org/api/fs.html#fspromisesutimespath-atime-mtime
40+
await utimes(pathToFile, 0, 0)
41+
42+
expect(fileContents).toBe(text)
43+
44+
heart = new Heart(pathToFile, mockIsActive(true))
45+
heart.beat()
46+
// HACK@jsjoeio - beat has some async logic but is not an async method
47+
// Therefore, we have to create an artificial wait in order to make sure
48+
// all async code has completed before asserting
49+
await new Promise((r) => setTimeout(r, 100))
50+
// Check that the heart wrote to the heartbeatFilePath and overwrote our text
51+
const fileContentsAfterBeat = await readFile(pathToFile, { encoding: "utf8" })
52+
expect(fileContentsAfterBeat).not.toBe(text)
53+
// Make sure the modified timestamp was updated.
54+
const fileStatusAfterEdit = await stat(pathToFile)
55+
expect(fileStatusAfterEdit.mtimeMs).toBeGreaterThan(0)
56+
})
57+
it("should log a warning when given an invalid file path", async () => {
58+
heart = new Heart(`fakeDir/fake.txt`, mockIsActive(false))
59+
heart.beat()
60+
// HACK@jsjoeio - beat has some async logic but is not an async method
61+
// Therefore, we have to create an artificial wait in order to make sure
62+
// all async code has completed before asserting
63+
await new Promise((r) => setTimeout(r, 100))
64+
expect(logger.warn).toHaveBeenCalled()
65+
})
66+
it("should be active after calling beat", () => {
67+
heart.beat()
68+
69+
const isAlive = heart.alive()
70+
expect(isAlive).toBe(true)
71+
})
72+
it("should not be active after dispose is called", () => {
73+
heart.dispose()
74+
75+
const isAlive = heart.alive()
76+
expect(isAlive).toBe(false)
77+
})
78+
})
79+
80+
describe("heartbeatTimer", () => {
81+
beforeAll(() => {
82+
mockLogger()
83+
})
84+
afterAll(() => {
85+
jest.restoreAllMocks()
86+
})
87+
afterEach(() => {
88+
jest.resetAllMocks()
89+
})
90+
it("should call beat when isActive resolves to true", async () => {
91+
const isActive = true
92+
const mockIsActive = jest.fn().mockResolvedValue(isActive)
93+
const mockBeatFn = jest.fn()
94+
await heartbeatTimer(mockIsActive, mockBeatFn)
95+
expect(mockIsActive).toHaveBeenCalled()
96+
expect(mockBeatFn).toHaveBeenCalled()
97+
})
98+
it("should log a warning when isActive rejects", async () => {
99+
const errorMsg = "oh no"
100+
const error = new Error(errorMsg)
101+
const mockIsActive = jest.fn().mockRejectedValue(error)
102+
const mockBeatFn = jest.fn()
103+
await heartbeatTimer(mockIsActive, mockBeatFn)
104+
expect(mockIsActive).toHaveBeenCalled()
105+
expect(mockBeatFn).not.toHaveBeenCalled()
106+
expect(logger.warn).toHaveBeenCalledWith(errorMsg)
107+
})
108+
})

0 commit comments

Comments
 (0)