aprs.me/test/aprsme_web/plugs/content_security_policy_test.exs
Graham McIntire 2f09b590e2
Some checks are pending
Elixir CI / Build and test (push) Waiting to run
Elixir CI / Dialyzer (push) Waiting to run
Elixir CI / Build and Push Docker Image (push) Blocked by required conditions
fix: resolve all credo issues — 0 warnings, 0 refactoring, 0 readability issues
- Add ex_slop credo plugin with all 40 checks
- Remove DualKeyAccess patterns across codebase — atom-only access
- Fix LengthInGuard, LengthComparison, ListLast, ReduceMapPut in lib/
- Disable LengthComparison for test files
- Remove obvious comments and narrator comments (~50)
- Add missing aliases for fully-qualified modules
- Rewrite boilerplate docs in test/support
- Add normalize_keys helpers at API boundaries
2026-07-29 10:54:07 -05:00

122 lines
4.3 KiB
Elixir

defmodule AprsmeWeb.Plugs.ContentSecurityPolicyTest do
use AprsmeWeb.ConnCase
alias AprsmeWeb.Plugs.ContentSecurityPolicy
describe "call/2" do
test "sets Content-Security-Policy header", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
csp = conn |> get_resp_header("content-security-policy") |> List.first()
assert is_binary(csp) and byte_size(csp) > 0
# Verify required directives are present
assert csp =~ "default-src 'self'"
assert csp =~ "object-src 'none'"
assert csp =~ "frame-ancestors 'none'"
assert csp =~ "base-uri 'self'"
assert csp =~ "form-action 'self'"
end
test "includes nonce in script-src", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
csp = conn |> get_resp_header("content-security-policy") |> List.first()
# Verify nonce-based script-src is used instead of unsafe-inline
assert csp =~ ~r/script-src[^;]*'nonce-[A-Za-z0-9+\/=]+'/
# Extract just the script-src directive and ensure it has no unsafe-inline
[script_src] = Regex.run(~r/script-src\s+([^;]+)/, csp, capture: :all_but_first)
refute script_src =~ "'unsafe-inline'"
end
test "stores nonce in conn.private", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
nonce = conn.private[:csp_nonce]
assert is_binary(nonce) and byte_size(nonce) > 0
csp = conn |> get_resp_header("content-security-policy") |> List.first()
assert csp =~ "'nonce-#{nonce}'"
end
test "generates a unique nonce per request", %{conn: conn} do
conn1 = ContentSecurityPolicy.call(conn, [])
conn2 = ContentSecurityPolicy.call(build_conn(), [])
nonce1 = conn1.private[:csp_nonce]
nonce2 = conn2.private[:csp_nonce]
assert nonce1 != nonce2
end
test "allows external script sources", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
csp = conn |> get_resp_header("content-security-policy") |> List.first()
# Plausible analytics (self-hosted)
assert csp =~ "script-src" and csp =~ "https://a.w5isp.com"
# Sentry error tracking
assert csp =~ "script-src" and csp =~ "https://js.sentry-cdn.com"
# Leaflet/map CDN
assert csp =~ "script-src" and csp =~ "https://unpkg.com"
end
test "allows map tile connect sources", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
csp = conn |> get_resp_header("content-security-policy") |> List.first()
assert csp =~ "connect-src" and csp =~ "https://tile.openstreetmap.org"
assert csp =~ "connect-src" and csp =~ "https://*.tile.openstreetmap.org"
assert csp =~ "connect-src" and csp =~ "https://*.basemaps.cartocdn.com"
assert csp =~ "connect-src" and csp =~ "wss:"
end
test "allows sentry connect sources", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
csp = conn |> get_resp_header("content-security-policy") |> List.first()
assert csp =~ "connect-src" and csp =~ "https://*.ingest.sentry.io"
assert csp =~ "connect-src" and csp =~ "https://*.sentry.io"
end
test "allows image sources from any HTTPS origin for map tiles", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
csp = conn |> get_resp_header("content-security-policy") |> List.first()
assert csp =~ "img-src" and csp =~ "https:"
assert csp =~ "img-src" and csp =~ "data:"
assert csp =~ "img-src" and csp =~ "blob:"
end
test "allows unsafe-inline for style-src (required by Tailwind and Phoenix)", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
csp = conn |> get_resp_header("content-security-policy") |> List.first()
# style-src still needs unsafe-inline for Tailwind/Phoenix inline styles
assert csp =~ "style-src" and csp =~ "'unsafe-inline'"
end
test "sets worker-src to allow web workers and blob workers", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
csp = conn |> get_resp_header("content-security-policy") |> List.first()
assert csp =~ "worker-src 'self' blob:"
end
test "blocks framing via frame-ancestors 'none'", %{conn: conn} do
conn = ContentSecurityPolicy.call(conn, [])
csp = conn |> get_resp_header("content-security-policy") |> List.first()
assert csp =~ "frame-ancestors 'none'"
end
end
end