Skip to content

Commit 7c566fc

Browse files
committed
test: split tests by source module
🤖 Generated with [OpenCode](https://opencode.ai) (Smart-router)
1 parent dc5c465 commit 7c566fc

10 files changed

Lines changed: 445 additions & 346 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"scripts": {
6363
"build": "rm -rf dist && tsc --project tsconfig.build.esm.json && tsc --project tsconfig.build.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
6464
"lint": "oxfmt && oxlint --fix && tsc --noEmit",
65-
"test": "node --test test/*.ts",
65+
"test": "node --test-concurrency=1 --test test/*.test.ts",
6666
"prepack": "pnpm build",
6767
"preuninstall": "node dist/esm/certs-cli.js -u",
6868
"start": "node src/cli.ts"

test/app-data-path.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import assert from "node:assert"
2+
import os from "node:os"
3+
import path from "node:path"
4+
import { afterEach, describe, it } from "node:test"
5+
6+
import { getAppDataPath } from "../src/app-data-path.ts"
7+
8+
describe("app-data-path", () => {
9+
const originalAppData = process.env["APPDATA"]
10+
11+
afterEach(() => {
12+
if (originalAppData === undefined) {
13+
delete process.env["APPDATA"]
14+
} else {
15+
process.env["APPDATA"] = originalAppData
16+
}
17+
})
18+
19+
it("returns base directory when no app name is supplied", () => {
20+
delete process.env["APPDATA"]
21+
const basePath = getAppDataPath()
22+
assert.ok(typeof basePath === "string" && basePath.length > 0)
23+
})
24+
25+
it("resolves app directory under platform base path", () => {
26+
delete process.env["APPDATA"]
27+
const appPath = getAppDataPath("https-localhost")
28+
const home = os.homedir()
29+
30+
switch (os.platform()) {
31+
case "darwin":
32+
assert.strictEqual(
33+
appPath,
34+
path.join(home, "Library", "Application Support", "https-localhost"),
35+
)
36+
break
37+
case "win32":
38+
assert.strictEqual(appPath, path.join(home, "AppData", "Roaming", "https-localhost"))
39+
break
40+
default:
41+
assert.strictEqual(appPath, path.join(home, ".config", "https-localhost"))
42+
}
43+
})
44+
45+
it("prefers APPDATA environment variable when defined", () => {
46+
process.env["APPDATA"] = "/tmp/custom-app-data"
47+
assert.strictEqual(getAppDataPath(), "/tmp/custom-app-data")
48+
assert.strictEqual(
49+
getAppDataPath("https-localhost"),
50+
path.join("/tmp/custom-app-data", "https-localhost"),
51+
)
52+
})
53+
})

test/certs.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import assert from "node:assert"
2+
import fs from "node:fs"
3+
import net from "node:net"
4+
import { afterEach, describe, it } from "node:test"
5+
import tls from "node:tls"
6+
7+
import { generate, getCerts, remove } from "../src/certs.ts"
8+
import { getEnv } from "../src/env.ts"
9+
10+
describe("certs", { timeout: 300000 }, () => {
11+
afterEach(() => {
12+
remove("test/custom-folder")
13+
remove("test/custom folder")
14+
delete process.env["CERT_PATH"]
15+
delete process.env["HOST"]
16+
})
17+
18+
it("can be uninstalled", () => {
19+
remove()
20+
})
21+
22+
it("uninstall is idempotent (doesn't fail if called twice)", () => {
23+
remove()
24+
})
25+
26+
it("can be installed", async () => {
27+
await generate()
28+
})
29+
30+
it("provides the certificate", async () => {
31+
const env = getEnv()
32+
const appCerts = await getCerts({
33+
domain: env.HOST,
34+
certPath: env.CERT_PATH,
35+
reinstall: env.REINSTALL,
36+
})
37+
const realCerts = await getCerts({
38+
domain: env.HOST,
39+
certPath: env.CERT_PATH,
40+
reinstall: env.REINSTALL,
41+
})
42+
assert.deepStrictEqual(appCerts, realCerts)
43+
})
44+
45+
it("works with environment domain", async () => {
46+
const appCerts = await getCerts({ domain: "192.168.0.1" })
47+
const secureContext = tls.createSecureContext({
48+
cert: appCerts.cert,
49+
})
50+
const secureSocket = new tls.TLSSocket(new net.Socket(), {
51+
secureContext,
52+
})
53+
const cert = secureSocket.getCertificate()
54+
assert(cert && "subjectaltname" in cert)
55+
const certDomain = cert.subjectaltname?.split(":")[1]
56+
57+
assert.strictEqual(certDomain, "192.168.0.1")
58+
})
59+
60+
it("crashes if certs do not exist in custom folder", async () => {
61+
const customCertPath = "test/custom-folder"
62+
await generate({ appDataPath: customCertPath })
63+
fs.unlinkSync("test/custom-folder/localhost.crt")
64+
fs.unlinkSync("test/custom-folder/localhost.key")
65+
66+
await assert.rejects(getCerts({ certPath: customCertPath }), /Certificates are missing/)
67+
remove(customCertPath)
68+
})
69+
})

test/cli.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import assert from "node:assert"
2+
import { spawn } from "node:child_process"
3+
import path from "node:path"
4+
import { describe, it } from "node:test"
5+
6+
describe("cli", () => {
7+
const cliPath = path.resolve("src/cli.ts")
8+
9+
it("CLI flags override environment", async () => {
10+
const testDir = path.resolve("test")
11+
const proc = spawn("node", [cliPath, "--port", "4448", testDir], {
12+
env: { ...process.env, PORT: "4447" },
13+
stdio: ["ignore", "pipe", "pipe"],
14+
})
15+
16+
try {
17+
await new Promise<void>((resolve, reject) => {
18+
proc.stdout.on("data", (data: Buffer) => {
19+
if (data.toString().includes("Server running on port 4448")) resolve()
20+
})
21+
proc.stderr.on("data", (data: Buffer) => {
22+
if (data.toString().includes("Server running on port 4448")) resolve()
23+
})
24+
proc.on("error", reject)
25+
setTimeout(() => reject(new Error("Timeout waiting for server")), 5000)
26+
})
27+
} finally {
28+
proc.kill("SIGTERM")
29+
}
30+
})
31+
32+
it("prints help when --help is passed", async () => {
33+
const proc = spawn("node", [cliPath, "--help"], {
34+
env: { ...process.env },
35+
stdio: ["ignore", "pipe", "pipe"],
36+
})
37+
38+
let stdout = ""
39+
await new Promise<void>((resolve, reject) => {
40+
proc.stdout.on("data", (data: Buffer) => {
41+
stdout += data.toString()
42+
})
43+
proc.on("close", () => resolve())
44+
proc.on("error", reject)
45+
})
46+
47+
assert.ok(stdout.includes("Usage: serve [options] [path]"))
48+
assert.ok(stdout.includes("-p, --port"))
49+
})
50+
})

test/env.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import assert from "node:assert"
2+
import { afterEach, describe, it } from "node:test"
3+
4+
import { envSchema, getEnv } from "../src/env.ts"
5+
6+
describe("env", () => {
7+
const originalEnv = { ...process.env }
8+
9+
afterEach(() => {
10+
process.env = { ...originalEnv }
11+
})
12+
13+
it("applies defaults when environment is empty", () => {
14+
delete process.env["PORT"]
15+
delete process.env["HOST"]
16+
delete process.env["CERT_PATH"]
17+
delete process.env["REINSTALL"]
18+
19+
const env = getEnv()
20+
assert.strictEqual(env.PORT, 443)
21+
assert.strictEqual(env.HOST, "localhost")
22+
assert.strictEqual(env.CERT_PATH, undefined)
23+
assert.strictEqual(env.REINSTALL, false)
24+
})
25+
26+
it("reads values from process.env", () => {
27+
process.env["PORT"] = "8443"
28+
process.env["HOST"] = "test.local"
29+
process.env["CERT_PATH"] = "/custom/path"
30+
process.env["REINSTALL"] = "true"
31+
32+
const env = getEnv()
33+
assert.strictEqual(env.PORT, 8443)
34+
assert.strictEqual(env.HOST, "test.local")
35+
assert.strictEqual(env.CERT_PATH, "/custom/path")
36+
assert.strictEqual(env.REINSTALL, true)
37+
})
38+
39+
it("allows explicit overrides to take precedence", () => {
40+
process.env["PORT"] = "8443"
41+
const env = getEnv({ PORT: 9443 })
42+
assert.strictEqual(env.PORT, 9443)
43+
})
44+
45+
it("rejects invalid ports", () => {
46+
assert.throws(() => envSchema.parse({ PORT: "invalid" }))
47+
assert.throws(() => envSchema.parse({ PORT: 0 }))
48+
assert.throws(() => envSchema.parse({ PORT: 70000 }))
49+
})
50+
})

test/helpers.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { execFileSync } from "node:child_process"
2+
import fs from "node:fs"
3+
import http from "node:http"
4+
import type { IncomingMessage, Server } from "node:http"
5+
import https from "node:https"
6+
import path from "node:path"
7+
8+
import { getAppDataPath } from "../src/app-data-path.ts"
9+
10+
const HTTPS_PORT = 4443
11+
const HTTP_PORT = 8080
12+
13+
function getRootCA(): Buffer | undefined {
14+
try {
15+
const certDir = getAppDataPath("https-localhost")
16+
const files = fs.readdirSync(certDir)
17+
const exe = files.find(file => file.startsWith("mkcert"))
18+
if (!exe) return undefined
19+
const caRoot = execFileSync(path.join(certDir, exe), ["-CAROOT"]).toString().trim()
20+
const rootCAPath = path.join(caRoot, "rootCA.pem")
21+
if (fs.existsSync(rootCAPath)) {
22+
return fs.readFileSync(rootCAPath)
23+
}
24+
} catch {
25+
// Ignore error if CA root cannot be found yet
26+
}
27+
return undefined
28+
}
29+
30+
async function closeServer(server: Server | undefined): Promise<void> {
31+
if (!server || !server.listening) return
32+
server.closeAllConnections()
33+
server.closeIdleConnections()
34+
await new Promise(resolve => server.close(resolve))
35+
}
36+
37+
async function makeRequest(
38+
requestPath = "/",
39+
secure = true,
40+
port: number | string = HTTPS_PORT,
41+
): Promise<{
42+
data: string
43+
statusCode?: number
44+
headers: Record<string, string | string[] | undefined>
45+
}> {
46+
const rootCA = secure ? getRootCA() : undefined
47+
const options: https.RequestOptions = {
48+
host: "localhost",
49+
port: port,
50+
path: requestPath,
51+
method: "GET",
52+
ca: rootCA ? [rootCA] : undefined,
53+
agent: false,
54+
}
55+
const protocol = secure ? https : http
56+
return new Promise((resolve, reject) => {
57+
protocol
58+
.request(options, (resp: IncomingMessage) => {
59+
let data = ""
60+
resp.on("data", (chunk: Buffer | string) => {
61+
data += chunk
62+
})
63+
resp.on("end", () =>
64+
resolve({
65+
data: data,
66+
statusCode: resp.statusCode,
67+
headers: resp.headers,
68+
}),
69+
)
70+
})
71+
.on("error", (err: Error) => reject(err))
72+
.end()
73+
})
74+
}
75+
76+
export { closeServer, HTTP_PORT, HTTPS_PORT, makeRequest }

test/import.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)