test(hosts): exhaustive matrix for the workstation API (auth, allowlist, payload, freshness) #21
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/vps-health-api#21
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-14-hosts-tests"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fourth link of the stacked series. Base:
issue-13-hosts-endpoints— review the diff against that branch, not againstmain.Closes #14.
What this is
#13 shipped
POST /hosts/<id>andGET /hostswith a 22-case smoke suite: enough to keep the expensive decisions from regressing silently, explicitly not the full matrix. This grows that file into the exhaustive matrix, because the ingestion route is the first publicly writable surface on the service — a mistake here is an intrusion, not an outage.__tests__/hosts.test.jsgoes from 22 to 155 cases. Suite total: 61 -> 194 (14 findings + 6 health + 19 auth unchanged, 155 hosts).index.jsis untouched — the only file in this PR is the test file.Coverage added
Authorizationheaders (wrong token, scheme alone, wrong case, no scheme,Basic, double space, one char short, one char long)HEALTH_TOKENunset still allows ingestion;HOSTS_INGEST_TOKENunset still allows readsGET/PUT/PATCH/DELETEon/hosts/<id>;POST /hosts; a query string that is not part of the id[auth]line naming the route and the id; a 404 leaves none (it never reached the gate); a hostileX-Real-IPcannot break out of its lineInfinityonly a raw body can express, a duplicate key, and the legitimate edges (uptime: 0, emptyloadAvg)0600mode, whitelist rebuild, 128-char cap onhostnameandcpu.model,loadAvgsliced to 3, route id winning over a body id, overwrite leaving no temp debris, nestedHOSTS_DIRcreated on demand, 500 when it cannot bereceivedAtclamped to 0, a customHOSTS_STALE_SECONDSand its 5 fallback casesneverSeenone; sorting; a file for a non-allowlisted id never served; 8 ways a snapshot file can be corrupt, each degrading its own entry; keys and oversized strings stripped on the way outTwo things worth a reviewer's attention
1. Malformed ids assert 404, never 403 — and that is load-bearing.
new URL()normalises/hosts/../../etc/passwdto/etc/passwd;..%2f..%2f,THINKPADand a 33-character id failHOST_ROUTE_RE. None of them reaches a handler, so 403 is reserved for ids that are well formed but absent from the allowlist. To check the tests actually bite rather than merely agreeing with today's behaviour,HOST_ROUTE_REwas temporarily widened to^/hosts/(.+)$: 16 tests turned red. The mutation was reverted (git checkout index.js); the branch never contained it.2. Freshness assertions freeze the clock. The 899/900/901 boundary is meaningless if the reported
ageSecondscan drift by a second because the machine was busy — the 900 case (online: true, inclusive threshold) would go intermittent. So those tests pinDate.now()to a fixed instant for the duration of the request. OnlyDate.nowis replaced, not the whole timer stack, sofetchand the http server keep their real timeouts. Second mutation check: flippingageSeconds <= HOSTS_STALE_SECONDSto<turns exactly the two inclusive-threshold tests red, and nothing else.One documented divergence from the issue
The acceptance criteria ask that "a 4 KiB
hostnameis absent from the written file". It is — but not for the reason the wording implies:MAX_BODY_BYTESis 4096, so a body carrying a 4096-character hostname is already over the ceiling and answers 413 before the sanitiser ever sees it. Both readings are therefore covered:No defect was found in the #13 implementation; every observed behaviour matches what the issue describes, this nuance aside.
Review order
#11 -> #12 -> #13 -> #14. This branch only adds tests, so it is safe to read last and on its own.Generated autonomously by /autopilot run of 2026-08-16
Verdict : APPROVE
155 tests dans
hosts.test.jsau tip. La discipline sur les bornes est reelle et rare : 4096 contre 4097 octets, id de 32 contre 33 caracteres, fraicheur 899/900/901 avec horloge gelee plutot qu'une course contre le wall-clock,1e999-> Infinity, cles JSON dupliquees,uptime: 0distingue d'un champ manquant, mode 0600, absence de debris temporaire, 500 sur repertoire non inscriptible,X-Real-IPhostile confine a une ligne d'ASCII imprimable,__proto__/constructor/prototypeabsents du texte du fichier, id de route prioritaire sur un id fourni dans le corps.Le choix de geler
Date.now()au lieu de tolerer une marge est le bon : les bornes de fraicheur sont exactes au lieu d'etre « environ N, sauf si la machine rame ».Suggestions (non bloquantes)
Les quatre tests 413 ne peuvent pas echouer pour la bonne raison.
ingestOversizedconvertit toute erreur de transport en413:Si le serveur cessait d'ecrire le 413 et se contentait de detruire la socket, les quatre resteraient verts. Le compromis est documente et l'assertion sur le systeme de fichiers porte du poids reel — mais le code de statut lui-meme n'est actuellement fige nulle part. Un seul test emettant un
http.requestbrut et lisantres.statusCodeavant la destruction de la socket fermerait le trou. C'est le seul chemin ou le contrat client (un statut, pas une erreur reseau) est precisement l'enjeu — et c'est aussi la seule chose que le corps de la PR #20 promet au consommateur pour ce cas.frozenRequestremplace leDate.nowglobal le temps d'un aller-retour reseau. Sans danger ici (tests sequentiels, et lereceivedAtdewriteHostSnapshotpasse parnew Date(), que le patch ne touche pas) — mais cette non-interference est load-bearing et non ecrite. Un futur test qui ferait un POST a l'interieur d'unfrozenRequestobtiendrait silencieusement unreceivedAtreel confronte a unnowgele. Une ligne de commentaire suffirait.Meme trou
/hostsque le point 1 de ma revue de #20 : c'etait la PR la mieux placee pour le fermer./hostsn'a ete ajoute qu'a la liste « the ingestion token opens nothing on the read side », jamais a la matrice en-tete-manquant, etauth.test.jsn'a toujours pas ete touche.Revue adversariale — maillon 4/5, revu contre sa base
issue-13-hosts-endpoints.Mergee localement sur
mainen fast-forward (pile chainee : l API de merge Forgejo ne peut pas traiter une pile dont la base n est pasmain). Tip integre :e181a96. Forgejo ne detecte pas un merge ff local, donc cette PR est fermee a la main — le code EST surmain.Pull request closed