74 lines
3.4 KiB
TypeScript
74 lines
3.4 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// The security headers of spec §6.4, and the two things a page may not do: load a script inline
|
|
// or reach an origin that is not this app's.
|
|
|
|
import { describe, expect, it } from "vitest"
|
|
import { contentSecurityPolicy, securityHeaders } from "../apps/web/src/server/headers"
|
|
|
|
const policy = contentSecurityPolicy({ nonce: "test-nonce" })
|
|
|
|
describe("the content security policy", () => {
|
|
it("allows a script only with this request's nonce", () => {
|
|
expect(policy).toContain("script-src 'self' 'nonce-test-nonce' 'strict-dynamic'")
|
|
expect(policy).not.toContain("script-src 'self' 'unsafe-inline'")
|
|
expect(policy).not.toContain("unsafe-eval")
|
|
})
|
|
|
|
it("refuses framing, a base tag and a plugin outright", () => {
|
|
expect(policy).toContain("frame-ancestors 'none'")
|
|
expect(policy).toContain("base-uri 'none'")
|
|
expect(policy).toContain("object-src 'none'")
|
|
})
|
|
|
|
it("names no origin but this app's", () => {
|
|
const sources = policy
|
|
.split("; ")
|
|
.flatMap((directive) => directive.split(" ").slice(1))
|
|
.filter((source) => source.includes("."))
|
|
expect(sources).toEqual([])
|
|
})
|
|
|
|
it("keeps fonts and connections on this origin, so no CDN or font host is reachable", () => {
|
|
expect(policy).toContain("font-src 'self'")
|
|
expect(policy).toContain("connect-src 'self'")
|
|
})
|
|
})
|
|
|
|
describe("the content security policy of a development run", () => {
|
|
it("allows eval for React's development build, and only there", () => {
|
|
expect(contentSecurityPolicy({ nonce: "n", development: true })).toContain("'strict-dynamic' 'unsafe-eval'")
|
|
expect(contentSecurityPolicy({ nonce: "n" })).not.toContain("unsafe-eval")
|
|
expect(contentSecurityPolicy({ nonce: "n", development: false })).not.toContain("unsafe-eval")
|
|
expect(securityHeaders({ nonce: "n", secure: false, development: true })["content-security-policy"]).toContain(
|
|
"unsafe-eval",
|
|
)
|
|
expect(securityHeaders({ nonce: "n", secure: true })["content-security-policy"]).not.toContain("unsafe-eval")
|
|
})
|
|
})
|
|
|
|
describe("the response headers", () => {
|
|
it("pin the transport only where there is one to pin", () => {
|
|
expect(securityHeaders({ nonce: "n", secure: true })["strict-transport-security"]).toContain("max-age=63072000")
|
|
expect(securityHeaders({ nonce: "n", secure: false })["strict-transport-security"]).toBeUndefined()
|
|
})
|
|
|
|
it("carry the rest of the baseline", () => {
|
|
const headers = securityHeaders({ nonce: "n", secure: true })
|
|
expect(headers["x-content-type-options"]).toBe("nosniff")
|
|
expect(headers["referrer-policy"]).toBe("strict-origin-when-cross-origin")
|
|
expect(headers["cross-origin-opener-policy"]).toBe("same-origin")
|
|
expect(headers["permissions-policy"]).toContain("camera=()")
|
|
})
|
|
})
|
|
|
|
describe("the browser's half of the API path", () => {
|
|
it("types a call by its contract's own schemas", async () => {
|
|
// A compile-time property, checked here by construction: `callContract` takes the contract's
|
|
// input type and answers its output type. If either drifted, this file would not typecheck.
|
|
const { callContract } = await import("../apps/web/src/ui/client")
|
|
const contract = { name: "notes.note.create", input: { _input: { title: "" } }, output: { _output: { id: "" } } }
|
|
expect(typeof callContract).toBe("function")
|
|
expect(contract.name).toBe("notes.note.create")
|
|
})
|
|
})
|