Add shift-left quality gates: cognitive complexity, coverage, dependency fix
All checks were successful
CI / build (push) Successful in 13m17s
CI / test (push) Successful in 3m59s
CI / coverage (push) Successful in 9m22s
CI / audit (push) Successful in 16s

Adds two new gates to the existing format/lint/test/audit pipeline
(Makefile.toml `cargo make ci`, .forgejo/workflows/ci.yml):

- Cognitive complexity via clippy's nursery cognitive_complexity lint
  (clippy.toml, threshold 15), scoped to --bins so test code's naturally
  higher branch count doesn't get gated. Went with this over the closest
  real cyclomatic-complexity tool (rust-code-analysis-cli) because that
  crate hasn't shipped a release since Jan 2023.
- Test coverage via cargo-llvm-cov, chosen over cargo-tarpaulin for
  friendlier behavior in containerized/dind CI (no ptrace). Report-only
  for now (no --fail-under-lines) since a real threshold needs real usage
  data first — see below. main.rs is excluded: it's orchestration glue
  exercised by the real end-to-end `cargo run`, not unit tests.

Getting both gates running required writing pkgwatch's first tests
(previously zero). To make the GitHub-facing modules unit-testable
without hitting real github.com/api.github.com, added `GithubEndpoints`
(src/github.rs) so checker/fetcher/verifier take injectable base URLs,
and added mockito + tempfile as dev-dependencies. Result: 27 tests,
94% region / 96% line coverage excluding main.rs.

Also: cargo audit (now wired into `cargo make ci`) immediately caught a
real, currently-open advisory (RUSTSEC-2026-0285, published days ago) in
the transitive rustls dependency — bumped 0.23.44 -> 0.23.45 to clear it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Austin Schaefer 2026-09-17 09:16:14 +02:00
parent 7f4bb4cccf
commit 13a1381bdf
12 changed files with 946 additions and 19 deletions

View file

@ -39,6 +39,12 @@ jobs:
- name: Clippy - name: Clippy
run: cargo clippy --all-targets -- -D warnings run: cargo clippy --all-targets -- -D warnings
# Cognitive complexity (clippy nursery lint — see clippy.toml for why
# not literal cyclomatic complexity), bin target only so test code's
# naturally higher complexity isn't gated on this.
- name: Complexity
run: cargo clippy --bins -- -D warnings -W clippy::cognitive_complexity
- name: Check release profile compiles - name: Check release profile compiles
run: cargo check --release run: cargo check --release
@ -70,6 +76,46 @@ jobs:
- name: Test - name: Test
run: cargo test run: cargo test
coverage:
needs: build
runs-on: rust-ci
steps:
- uses: actions/checkout@v4
- name: Cache cargo registry and build artifacts
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
restore-keys: |
cargo-${{ runner.os }}-
- name: Cache sccache compilation objects
uses: actions/cache@v4
with:
path: /root/.cache/sccache
key: sccache-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
sccache-${{ runner.os }}-
# Not baked into the rust-ci image (see docker/rust-ci in
# infrastructure) — install fresh each run rather than bumping the
# shared image just for this one project.
- name: Install cargo-llvm-cov
run: |
rustup component add llvm-tools-preview
command -v cargo-llvm-cov >/dev/null 2>&1 || cargo install cargo-llvm-cov --locked
# Reports coverage only — no --fail-under-lines yet. main.rs is
# excluded: thin orchestration glue exercised by the real end-to-end
# `cargo run` against live GitHub, not unit tests, so it's not a
# meaningful signal here. See Makefile.toml > coverage-report.
- name: Coverage
run: cargo llvm-cov --ignore-filename-regex 'main\.rs' --summary-only
audit: audit:
needs: test needs: test
runs-on: rust-ci runs-on: rust-ci

322
Cargo.lock generated
View file

@ -17,6 +17,16 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@ -117,7 +127,7 @@ checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures",
"rand_core", "rand_core 0.10.1",
] ]
[[package]] [[package]]
@ -129,6 +139,15 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "colored"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.8" version = "4.6.8"
@ -213,12 +232,34 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "fastrand"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.12" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.2" version = "1.2.2"
@ -296,6 +337,18 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi 5.3.0",
"wasip2",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.4.3" version = "0.4.3"
@ -305,11 +358,30 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"r-efi", "r-efi 6.0.0",
"rand_core", "rand_core 0.10.1",
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "h2"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.1" version = "0.17.1"
@ -361,6 +433,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hybrid-array" name = "hybrid-array"
version = "0.4.15" version = "0.4.15"
@ -380,9 +458,11 @@ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"h2",
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
@ -630,12 +710,27 @@ version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.3" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.34" version = "0.4.34"
@ -665,6 +760,31 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "mockito"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0"
dependencies = [
"assert-json-diff",
"bytes",
"colored",
"futures-core",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"log",
"pin-project-lite",
"rand 0.9.5",
"regex",
"serde_json",
"serde_urlencoded",
"similar",
"tokio",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
@ -677,6 +797,29 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -701,10 +844,12 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hex", "hex",
"mockito",
"regex", "regex",
"reqwest", "reqwest",
"serde", "serde",
"sha2", "sha2",
"tempfile",
"toml", "toml",
] ]
@ -717,6 +862,15 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.107" version = "1.0.107"
@ -756,7 +910,7 @@ dependencies = [
"bytes", "bytes",
"getrandom 0.4.3", "getrandom 0.4.3",
"lru-slab", "lru-slab",
"rand", "rand 0.10.2",
"rand_pcg", "rand_pcg",
"ring", "ring",
"rustc-hash", "rustc-hash",
@ -792,12 +946,28 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "r-efi" name = "r-efi"
version = "6.0.0" version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [
"rand_chacha",
"rand_core 0.9.5",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.10.2" version = "0.10.2"
@ -806,7 +976,26 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [ dependencies = [
"chacha20", "chacha20",
"getrandom 0.4.3", "getrandom 0.4.3",
"rand_core", "rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
] ]
[[package]] [[package]]
@ -821,7 +1010,16 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [ dependencies = [
"rand_core", "rand_core 0.10.1",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
] ]
[[package]] [[package]]
@ -922,10 +1120,23 @@ dependencies = [
] ]
[[package]] [[package]]
name = "rustls" name = "rustix"
version = "0.23.44" version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"once_cell", "once_cell",
@ -1002,6 +1213,12 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -1020,6 +1237,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "3.7.0" version = "3.7.0"
@ -1101,6 +1324,18 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.11.0" version = "0.11.0"
@ -1134,6 +1369,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@ -1210,6 +1451,19 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.20" version = "2.0.20"
@ -1264,6 +1518,7 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@ -1279,6 +1534,20 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-util"
version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"libc",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "1.1.6+spec-1.1.0" version = "1.1.6+spec-1.1.0"
@ -1449,6 +1718,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
"wit-bindgen",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.128" version = "0.2.128"
@ -1636,6 +1914,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.4" version = "0.6.4"
@ -1665,6 +1949,26 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zerocopy"
version = "0.8.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.8" version = "0.1.8"

View file

@ -17,3 +17,7 @@ reqwest = { version = "0.13.5", default-features = false, features = ["blocking"
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
sha2 = "0.11.0" sha2 = "0.11.0"
toml = "1.1.6" toml = "1.1.6"
[dev-dependencies]
mockito = "1.7.2"
tempfile = "3.27.0"

View file

@ -13,10 +13,30 @@ args = ["fmt", "--check"]
command = "cargo" command = "cargo"
args = ["clippy", "--all-targets", "--", "-D", "warnings"] args = ["clippy", "--all-targets", "--", "-D", "warnings"]
# Cognitive complexity (clippy's nursery lint, not literal cyclomatic
# complexity — see clippy.toml for why) on the bin target only: run without
# --all-targets so #[cfg(test)] code, which naturally reads as more
# "complex" without being a maintainability signal, isn't gated on this.
[tasks.complexity]
command = "cargo"
args = ["clippy", "--bins", "--", "-D", "warnings", "-W", "clippy::cognitive_complexity"]
[tasks.test] [tasks.test]
command = "cargo" command = "cargo"
args = ["test"] args = ["test"]
# Named coverage-report, not coverage: cargo-make reserves "coverage" for
# its own built-in tarpaulin/kcov-routing composite task, which clobbers a
# same-named custom one.
#
# Reports coverage only; not gated on a threshold yet — main.rs is thin
# orchestration glue exercised by the real end-to-end `cargo run`, not unit
# tests, so it's excluded here rather than dragging the number down for
# reasons unrelated to test quality.
[tasks.coverage-report]
command = "cargo"
args = ["llvm-cov", "--ignore-filename-regex", "main\\.rs", "--summary-only"]
[tasks.audit] [tasks.audit]
command = "cargo" command = "cargo"
args = ["audit"] args = ["audit"]
@ -31,7 +51,7 @@ command = "git"
args = ["config", "core.hooksPath", ".githooks"] args = ["config", "core.hooksPath", ".githooks"]
[tasks.ci] [tasks.ci]
dependencies = ["format-check", "lint", "test", "audit"] dependencies = ["format-check", "lint", "complexity", "test", "coverage-report", "audit"]
[tasks.default] [tasks.default]
alias = "ci" alias = "ci"

9
clippy.toml Normal file
View file

@ -0,0 +1,9 @@
# Shift-left complexity gate (see `cargo make complexity`). Not literal
# McCabe cyclomatic complexity — clippy's nursery-tier cognitive_complexity
# lint, chosen over the closest real cyclomatic-complexity tool
# (rust-code-analysis-cli) because that crate hasn't been released since
# early 2023. Threshold set well above the current codebase's max (5, in
# `main`) so it flags genuinely tangled control flow without nagging on
# today's code; revisit if legitimate future complexity (e.g. the
# build/publish pipeline) needs more room.
cognitive-complexity-threshold = 15

View file

@ -1,3 +1,4 @@
use crate::github::GithubEndpoints;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use regex::Regex; use regex::Regex;
@ -14,8 +15,12 @@ use regex::Regex;
/// 404s) right after each real release, and that tag sorts newest in the /// 404s) right after each real release, and that tag sorts newest in the
/// feed. So each candidate is confirmed against the releases API in feed /// feed. So each candidate is confirmed against the releases API in feed
/// order, returning the first that actually resolves. /// order, returning the first that actually resolves.
pub fn latest_github_release(client: &reqwest::blocking::Client, repo: &str) -> Result<String> { pub fn latest_github_release(
let url = format!("https://github.com/{repo}/releases.atom"); client: &reqwest::blocking::Client,
endpoints: &GithubEndpoints,
repo: &str,
) -> Result<String> {
let url = format!("{}/{repo}/releases.atom", endpoints.web);
let body = client.get(&url).send()?.error_for_status()?.text()?; let body = client.get(&url).send()?.error_for_status()?.text()?;
let re = Regex::new(r#"releases/tag/([^"]+)""#)?; let re = Regex::new(r#"releases/tag/([^"]+)""#)?;
@ -28,7 +33,7 @@ pub fn latest_github_release(client: &reqwest::blocking::Client, repo: &str) ->
} }
for tag in candidates { for tag in candidates {
let release_url = format!("https://api.github.com/repos/{repo}/releases/tags/{tag}"); let release_url = format!("{}/repos/{repo}/releases/tags/{tag}", endpoints.api);
if client.get(&release_url).send()?.status().is_success() { if client.get(&release_url).send()?.status().is_success() {
return Ok(tag); return Ok(tag);
} }
@ -46,3 +51,98 @@ pub fn latest_github_release(client: &reqwest::blocking::Client, repo: &str) ->
pub fn version_from_tag(tag: &str) -> &str { pub fn version_from_tag(tag: &str) -> &str {
tag.strip_prefix('v').unwrap_or(tag) tag.strip_prefix('v').unwrap_or(tag)
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_from_tag_strips_leading_v() {
assert_eq!(version_from_tag("v2.62.0"), "2.62.0");
}
#[test]
fn version_from_tag_leaves_bare_version_unchanged() {
assert_eq!(version_from_tag("0.12.15"), "0.12.15");
}
fn atom_feed(tags: &[&str]) -> String {
let entries: String = tags
.iter()
.map(|t| {
format!(r#"<link rel="alternate" href="https://github.com/o/r/releases/tag/{t}"/>"#)
})
.collect();
format!("<feed>{entries}</feed>")
}
#[test]
fn latest_github_release_skips_tags_with_no_real_release() {
let mut server = mockito::Server::new();
let endpoints = GithubEndpoints {
web: server.url(),
api: server.url(),
};
// Mirrors the real scaleway-cli case: newest feed entry (a -dbg1
// tag) has no Release object behind it and 404s.
let _feed = server
.mock("GET", "/o/r/releases.atom")
.with_status(200)
.with_body(atom_feed(&["v2.62.0-dbg1", "v2.62.0"]))
.create();
let _missing = server
.mock("GET", "/repos/o/r/releases/tags/v2.62.0-dbg1")
.with_status(404)
.create();
let _real = server
.mock("GET", "/repos/o/r/releases/tags/v2.62.0")
.with_status(200)
.with_body("{}")
.create();
let client = reqwest::blocking::Client::new();
let tag = latest_github_release(&client, &endpoints, "o/r").unwrap();
assert_eq!(tag, "v2.62.0");
}
#[test]
fn latest_github_release_errors_when_feed_has_no_tags() {
let mut server = mockito::Server::new();
let endpoints = GithubEndpoints {
web: server.url(),
api: server.url(),
};
let _feed = server
.mock("GET", "/o/r/releases.atom")
.with_status(200)
.with_body("<feed></feed>")
.create();
let client = reqwest::blocking::Client::new();
let err = latest_github_release(&client, &endpoints, "o/r").unwrap_err();
assert!(err.to_string().contains("no release tag found"));
}
#[test]
fn latest_github_release_errors_when_no_candidate_resolves() {
let mut server = mockito::Server::new();
let endpoints = GithubEndpoints {
web: server.url(),
api: server.url(),
};
let _feed = server
.mock("GET", "/o/r/releases.atom")
.with_status(200)
.with_body(atom_feed(&["v1.0.0-dbg1"]))
.create();
let _missing = server
.mock("GET", "/repos/o/r/releases/tags/v1.0.0-dbg1")
.with_status(404)
.create();
let client = reqwest::blocking::Client::new();
let err = latest_github_release(&client, &endpoints, "o/r").unwrap_err();
assert!(err.to_string().contains("resolved to a real release"));
}
}

View file

@ -58,3 +58,119 @@ pub fn load_packages_dir(dir: &Path) -> Result<Vec<(String, Package)>> {
} }
Ok(out) Ok(out)
} }
#[cfg(test)]
mod tests {
use super::*;
fn write(dir: &Path, name: &str, contents: &str) {
std::fs::write(dir.join(name), contents).unwrap();
}
#[test]
fn tier_matches_verification_method() {
assert_eq!(
Verification::SameOriginSha256 {
checksum_asset_pattern: "x".to_string()
}
.tier(),
4
);
assert_eq!(Verification::GithubAttestation.tier(), 2);
}
#[test]
fn loads_same_origin_sha256_package() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"pkg.toml",
r#"
[package.pkg]
repo = "o/r"
asset_pattern = "pkg_{version}_linux_amd64"
[package.pkg.verification]
method = "same-origin-sha256"
checksum_asset_pattern = "SHA256SUMS"
"#,
);
let packages = load_packages_dir(dir.path()).unwrap();
assert_eq!(packages.len(), 1);
let (name, pkg) = &packages[0];
assert_eq!(name, "pkg");
assert_eq!(pkg.repo, "o/r");
assert_eq!(pkg.asset_pattern, "pkg_{version}_linux_amd64");
assert_eq!(pkg.verification.tier(), 4);
}
#[test]
fn loads_github_attestation_package() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"pkg.toml",
r#"
[package.pkg]
repo = "o/r"
asset_pattern = "pkg-x86_64.tar.gz"
[package.pkg.verification]
method = "github-attestation"
"#,
);
let packages = load_packages_dir(dir.path()).unwrap();
assert_eq!(packages[0].1.verification.tier(), 2);
}
#[test]
fn loads_multiple_files_and_ignores_non_toml() {
let dir = tempfile::tempdir().unwrap();
write(
dir.path(),
"a.toml",
r#"
[package.a]
repo = "o/a"
asset_pattern = "a.tar.gz"
[package.a.verification]
method = "github-attestation"
"#,
);
write(
dir.path(),
"b.toml",
r#"
[package.b]
repo = "o/b"
asset_pattern = "b.tar.gz"
[package.b.verification]
method = "github-attestation"
"#,
);
write(dir.path(), "README.md", "not a package file");
let mut names: Vec<String> = load_packages_dir(dir.path())
.unwrap()
.into_iter()
.map(|(name, _)| name)
.collect();
names.sort();
assert_eq!(names, vec!["a", "b"]);
}
#[test]
fn errors_on_invalid_toml() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), "bad.toml", "not valid toml [[[");
assert!(load_packages_dir(dir.path()).is_err());
}
#[test]
fn empty_dir_yields_empty_list() {
let dir = tempfile::tempdir().unwrap();
assert!(load_packages_dir(dir.path()).unwrap().is_empty());
}
}

View file

@ -1,3 +1,4 @@
use crate::github::GithubEndpoints;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::Deserialize; use serde::Deserialize;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -17,12 +18,13 @@ struct Asset {
/// into `dest_dir`, returning the local path. /// into `dest_dir`, returning the local path.
pub fn download_asset( pub fn download_asset(
client: &reqwest::blocking::Client, client: &reqwest::blocking::Client,
endpoints: &GithubEndpoints,
repo: &str, repo: &str,
tag: &str, tag: &str,
asset_name: &str, asset_name: &str,
dest_dir: &Path, dest_dir: &Path,
) -> Result<PathBuf> { ) -> Result<PathBuf> {
let api_url = format!("https://api.github.com/repos/{repo}/releases/tags/{tag}"); let api_url = format!("{}/repos/{repo}/releases/tags/{tag}", endpoints.api);
let release: Release = client let release: Release = client
.get(&api_url) .get(&api_url)
.send()? .send()?
@ -46,3 +48,99 @@ pub fn download_asset(
std::fs::write(&dest_path, &bytes)?; std::fs::write(&dest_path, &bytes)?;
Ok(dest_path) Ok(dest_path)
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn download_asset_writes_matching_asset_to_dest_dir() {
let mut server = mockito::Server::new();
let endpoints = GithubEndpoints {
web: server.url(),
api: server.url(),
};
let asset_url = format!("{}/download/thing.tar.gz", server.url());
let release_body = format!(
r#"{{"assets": [{{"name": "thing.tar.gz", "browser_download_url": "{asset_url}"}}]}}"#
);
let _release = server
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
.with_status(200)
.with_body(release_body)
.create();
let _download = server
.mock("GET", "/download/thing.tar.gz")
.with_status(200)
.with_body(b"artifact-bytes".as_slice())
.create();
let client = reqwest::blocking::Client::new();
let dest_dir = tempfile::tempdir().unwrap();
let path = download_asset(
&client,
&endpoints,
"o/r",
"v1.0.0",
"thing.tar.gz",
dest_dir.path(),
)
.unwrap();
assert_eq!(path, dest_dir.path().join("thing.tar.gz"));
assert_eq!(std::fs::read(&path).unwrap(), b"artifact-bytes");
}
#[test]
fn download_asset_errors_when_no_asset_matches() {
let mut server = mockito::Server::new();
let endpoints = GithubEndpoints {
web: server.url(),
api: server.url(),
};
let _release = server
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
.with_status(200)
.with_body(r#"{"assets": [{"name": "other", "browser_download_url": "https://example.invalid/other"}]}"#)
.create();
let client = reqwest::blocking::Client::new();
let dest_dir = tempfile::tempdir().unwrap();
let err = download_asset(
&client,
&endpoints,
"o/r",
"v1.0.0",
"thing.tar.gz",
dest_dir.path(),
)
.unwrap_err();
assert!(err.to_string().contains("no asset named"));
}
#[test]
fn download_asset_errors_when_release_not_found() {
let mut server = mockito::Server::new();
let endpoints = GithubEndpoints {
web: server.url(),
api: server.url(),
};
let _release = server
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
.with_status(404)
.create();
let client = reqwest::blocking::Client::new();
let dest_dir = tempfile::tempdir().unwrap();
let err = download_asset(
&client,
&endpoints,
"o/r",
"v1.0.0",
"thing.tar.gz",
dest_dir.path(),
)
.unwrap_err();
assert!(err.to_string().contains("fetching release metadata"));
}
}

29
src/github.rs Normal file
View file

@ -0,0 +1,29 @@
/// Base URLs for GitHub's public web host (Atom feeds, release pages) and
/// its REST API, factored out so tests can point both at a local mock
/// server instead of the real github.com/api.github.com.
#[derive(Debug, Clone)]
pub struct GithubEndpoints {
pub web: String,
pub api: String,
}
impl Default for GithubEndpoints {
fn default() -> Self {
Self {
web: "https://github.com".to_string(),
api: "https://api.github.com".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_points_at_real_github() {
let endpoints = GithubEndpoints::default();
assert_eq!(endpoints.web, "https://github.com");
assert_eq!(endpoints.api, "https://api.github.com");
}
}

View file

@ -1,10 +1,12 @@
mod checker; mod checker;
mod config; mod config;
mod fetcher; mod fetcher;
mod github;
mod state; mod state;
mod verifier; mod verifier;
use anyhow::Result; use anyhow::Result;
use github::GithubEndpoints;
use std::path::Path; use std::path::Path;
/// First iteration: check -> fetch -> verify -> report, for whatever is /// First iteration: check -> fetch -> verify -> report, for whatever is
@ -13,6 +15,7 @@ fn main() -> Result<()> {
let client = reqwest::blocking::Client::builder() let client = reqwest::blocking::Client::builder()
.user_agent("pkgwatch/0.1 (PoC; https://code.austinschaefer.com)") .user_agent("pkgwatch/0.1 (PoC; https://code.austinschaefer.com)")
.build()?; .build()?;
let endpoints = GithubEndpoints::default();
let packages_dir = Path::new("packages.d"); let packages_dir = Path::new("packages.d");
let state_dir = Path::new("state"); let state_dir = Path::new("state");
@ -27,7 +30,7 @@ fn main() -> Result<()> {
for (name, pkg) in packages { for (name, pkg) in packages {
println!("== {name} ({}) ==", pkg.repo); println!("== {name} ({}) ==", pkg.repo);
let latest = checker::latest_github_release(&client, &pkg.repo)?; let latest = checker::latest_github_release(&client, &endpoints, &pkg.repo)?;
let last_seen = state::load_last_version(state_dir, &name); let last_seen = state::load_last_version(state_dir, &name);
if last_seen.as_deref() == Some(latest.as_str()) { if last_seen.as_deref() == Some(latest.as_str()) {
@ -41,12 +44,19 @@ fn main() -> Result<()> {
let asset_name = pkg let asset_name = pkg
.asset_pattern .asset_pattern
.replace("{version}", checker::version_from_tag(&latest)); .replace("{version}", checker::version_from_tag(&latest));
let artifact_path = let artifact_path = fetcher::download_asset(
fetcher::download_asset(&client, &pkg.repo, &latest, &asset_name, &dest_dir)?; &client,
&endpoints,
&pkg.repo,
&latest,
&asset_name,
&dest_dir,
)?;
println!(" fetched {}", artifact_path.display()); println!(" fetched {}", artifact_path.display());
let result = verifier::verify( let result = verifier::verify(
&client, &client,
&endpoints,
&pkg.verification, &pkg.verification,
&pkg.repo, &pkg.repo,
&latest, &latest,

View file

@ -16,3 +16,45 @@ pub fn save_last_version(state_dir: &Path, name: &str, version: &str) -> Result<
std::fs::write(state_dir.join(format!("{name}.version")), version)?; std::fs::write(state_dir.join(format!("{name}.version")), version)?;
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_last_version_missing_file_returns_none() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(load_last_version(dir.path(), "nonexistent"), None);
}
#[test]
fn save_then_load_roundtrips() {
let dir = tempfile::tempdir().unwrap();
save_last_version(dir.path(), "uv", "0.12.15").unwrap();
assert_eq!(
load_last_version(dir.path(), "uv"),
Some("0.12.15".to_string())
);
}
#[test]
fn load_last_version_trims_trailing_whitespace() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("uv.version"), "0.12.15\n").unwrap();
assert_eq!(
load_last_version(dir.path(), "uv"),
Some("0.12.15".to_string())
);
}
#[test]
fn save_last_version_creates_state_dir_if_missing() {
let dir = tempfile::tempdir().unwrap();
let nested = dir.path().join("nested/state");
save_last_version(&nested, "uv", "0.12.15").unwrap();
assert_eq!(
load_last_version(&nested, "uv"),
Some("0.12.15".to_string())
);
}
}

View file

@ -1,6 +1,7 @@
use crate::checker::version_from_tag; use crate::checker::version_from_tag;
use crate::config::Verification; use crate::config::Verification;
use crate::fetcher; use crate::fetcher;
use crate::github::GithubEndpoints;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::path::Path; use std::path::Path;
@ -17,6 +18,7 @@ pub struct VerificationResult {
/// each tier does and does not prove. /// each tier does and does not prove.
pub fn verify( pub fn verify(
client: &reqwest::blocking::Client, client: &reqwest::blocking::Client,
endpoints: &GithubEndpoints,
verification: &Verification, verification: &Verification,
repo: &str, repo: &str,
tag: &str, tag: &str,
@ -29,8 +31,14 @@ pub fn verify(
} => { } => {
let checksum_asset_name = let checksum_asset_name =
checksum_asset_pattern.replace("{version}", version_from_tag(tag)); checksum_asset_pattern.replace("{version}", version_from_tag(tag));
let checksum_path = let checksum_path = fetcher::download_asset(
fetcher::download_asset(client, repo, tag, &checksum_asset_name, dest_dir)?; client,
endpoints,
repo,
tag,
&checksum_asset_name,
dest_dir,
)?;
let checksum_text = std::fs::read_to_string(&checksum_path)?; let checksum_text = std::fs::read_to_string(&checksum_path)?;
let artifact_name = artifact_path let artifact_name = artifact_path
.file_name() .file_name()
@ -127,3 +135,144 @@ fn expected_checksum(checksum_text: &str, artifact_name: &str) -> Result<String>
lines.len() lines.len()
) )
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Verification;
#[test]
fn expected_checksum_bare_single_hash_file() {
let text = "f97935763c04be3e692460a7aaeaaab8fc3b78fcf8b389da820b38ae7423a638\n";
assert_eq!(
expected_checksum(text, "anything.tar.gz").unwrap(),
"f97935763c04be3e692460a7aaeaaab8fc3b78fcf8b389da820b38ae7423a638"
);
}
#[test]
fn expected_checksum_matches_gnu_style_line_by_filename() {
let text = "aaaa other-asset\nbbbb scaleway-cli_2.62.0_linux_amd64\ncccc more\n";
assert_eq!(
expected_checksum(text, "scaleway-cli_2.62.0_linux_amd64").unwrap(),
"bbbb"
);
}
#[test]
fn expected_checksum_matches_bsd_style_line_strips_star() {
let text = "aaaa *source.tar.gz\nbbbb *thing.tar.gz\n";
assert_eq!(expected_checksum(text, "thing.tar.gz").unwrap(), "bbbb");
}
#[test]
fn expected_checksum_is_case_insensitive() {
let text = "ABCDEF thing.tar.gz\n";
assert_eq!(expected_checksum(text, "thing.tar.gz").unwrap(), "abcdef");
}
#[test]
fn expected_checksum_errors_when_multi_line_file_has_no_match() {
let err = expected_checksum("aaaa other\nbbbb another\n", "thing.tar.gz").unwrap_err();
assert!(err.to_string().contains("no checksum entry"));
}
#[test]
fn expected_checksum_errors_on_empty_file() {
let err = expected_checksum("", "thing.tar.gz").unwrap_err();
assert!(err.to_string().contains("no checksum entry"));
}
#[test]
fn verify_same_origin_sha256_passes_on_matching_checksum() {
let mut server = mockito::Server::new();
let endpoints = GithubEndpoints {
web: server.url(),
api: server.url(),
};
let dest_dir = tempfile::tempdir().unwrap();
let artifact_path = dest_dir.path().join("thing.tar.gz");
std::fs::write(&artifact_path, b"hello world").unwrap();
let expected_hash = sha256_hex(b"hello world");
let release_url = format!("{}/download/SHA256SUMS", server.url());
let release_body = format!(
r#"{{"assets": [{{"name": "SHA256SUMS", "browser_download_url": "{release_url}"}}]}}"#
);
let _release = server
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
.with_status(200)
.with_body(release_body)
.create();
let _sums = server
.mock("GET", "/download/SHA256SUMS")
.with_status(200)
.with_body(format!("{expected_hash} thing.tar.gz\n"))
.create();
let verification = Verification::SameOriginSha256 {
checksum_asset_pattern: "SHA256SUMS".to_string(),
};
let client = reqwest::blocking::Client::new();
let result = verify(
&client,
&endpoints,
&verification,
"o/r",
"v1.0.0",
&artifact_path,
dest_dir.path(),
)
.unwrap();
assert!(result.passed);
assert_eq!(result.tier, 4);
}
#[test]
fn verify_same_origin_sha256_fails_on_mismatched_checksum() {
let mut server = mockito::Server::new();
let endpoints = GithubEndpoints {
web: server.url(),
api: server.url(),
};
let dest_dir = tempfile::tempdir().unwrap();
let artifact_path = dest_dir.path().join("thing.tar.gz");
std::fs::write(&artifact_path, b"hello world").unwrap();
let release_url = format!("{}/download/SHA256SUMS", server.url());
let release_body = format!(
r#"{{"assets": [{{"name": "SHA256SUMS", "browser_download_url": "{release_url}"}}]}}"#
);
let _release = server
.mock("GET", "/repos/o/r/releases/tags/v1.0.0")
.with_status(200)
.with_body(release_body)
.create();
let _sums = server
.mock("GET", "/download/SHA256SUMS")
.with_status(200)
.with_body(
"0000000000000000000000000000000000000000000000000000000000000000 thing.tar.gz\n",
)
.create();
let verification = Verification::SameOriginSha256 {
checksum_asset_pattern: "SHA256SUMS".to_string(),
};
let client = reqwest::blocking::Client::new();
let result = verify(
&client,
&endpoints,
&verification,
"o/r",
"v1.0.0",
&artifact_path,
dest_dir.path(),
)
.unwrap();
assert!(!result.passed);
assert!(result.justification.contains("mismatch"));
}
}