erplibre/script/qemu_cache/detient_test.go

190 lines
7 KiB
Go
Raw Permalink Normal View History

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
package main
import (
"bytes"
"io"
"net/http"
"net/url"
"os"
"strings"
"testing"
"time"
)
// « --detient » dit ce que le magasin tient, une ligne par question, avec la
// clé que le service calcule. Le magasin est rempli ici PAR le service, pour
// que toute divergence entre les deux calculs de clé se voie.
func TestDetientDitCeQueLeMagasinTient(t *testing.T) {
paquet := "/core/os/x86_64/outil-1.0-1-x86_64.pkg.tar.zst"
a := nouvelAmontScripte(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/installer.sh":
rediriger(http.StatusFound, "https://ailleurs.example/bootstrap.sh")(w, r)
case "/depot/etat":
servir("état courant")(w, r)
default:
servir("contenu du paquet")(w, r)
}
})
p := proxyDeTest(t)
for _, c := range []string{"/installer.sh", "/depot/etat", paquet} {
joue(t, p, "GET", a.hote(), c)
}
joue(t, p, "HEAD", a.hote(), "/installer.sh")
base := "http://" + a.hote()
questions := []string{
"GET " + base + "/depot/etat",
"GET " + base + "/installer.sh",
"HEAD " + base + "/installer.sh",
"get " + base + "/jamais-vu.sh",
"POST " + base + "/depot/etat",
"GET " + base + "/o/d.git/info/refs?service=git-upload-pack",
// Un paquet est portable : le même chemin sur un autre miroir est
// le même fichier.
"GET http://autre-miroir.example.invalid" + paquet,
"",
"# une ligne de commentaire",
base + "/depot/etat",
"GET pas-une-url",
}
var sortie bytes.Buffer
if err := EcrireDetentions(
p.Store, strings.NewReader(strings.Join(questions, "\n")), &sortie,
); err != nil {
t.Fatal(err)
}
attendus := []struct {
verdict, statut, classe, methode, url string
date bool
}{
{"garde", "200", "volatile", "GET", base + "/depot/etat", true},
{"statut", "302", "volatile", "GET", base + "/installer.sh", true},
{"statut", "302", "volatile", "HEAD", base + "/installer.sh", true},
{"absent", "-", "volatile", "GET", base + "/jamais-vu.sh", false},
{"non-cachable", "-", "volatile", "POST", base + "/depot/etat", false},
{"non-cachable", "-", "no-store", "GET",
base + "/o/d.git/info/refs?service=git-upload-pack", false},
{"garde", "200", "immutable", "GET",
"http://autre-miroir.example.invalid" + paquet, true},
{"garde", "200", "volatile", "GET", base + "/depot/etat", true},
{"non-cachable", "-", "-", "GET", "pas-une-url", false},
}
lignes := strings.Split(strings.TrimRight(sortie.String(), "\n"), "\n")
if len(lignes) != len(attendus) {
t.Fatalf("%d lignes, attendu %d :\n%s", len(lignes), len(attendus), sortie.String())
}
for i, a := range attendus {
champs := strings.Split(lignes[i], "\t")
if len(champs) != 6 {
t.Errorf("ligne %d : %d champs, attendu 6 : %q", i+1, len(champs), lignes[i])
continue
}
if champs[0] != a.verdict || champs[1] != a.statut ||
champs[3] != a.classe || champs[4] != a.methode || champs[5] != a.url {
t.Errorf("ligne %d : %q, attendu %s %s … %s %s %s",
i+1, lignes[i], a.verdict, a.statut, a.classe, a.methode, a.url)
}
if a.date {
if _, err := time.Parse(time.RFC3339, champs[2]); err != nil {
t.Errorf("ligne %d : date %q illisible", i+1, champs[2])
}
} else if champs[2] != "-" {
t.Errorf("ligne %d : date %q pour un objet absent", i+1, champs[2])
}
}
}
// Le relevé ne rajeunit rien : l'âge d'un objet est celui de son dernier
// SERVICE, et une question posée au magasin n'en est pas un.
func TestDetientNeRajeunitPas(t *testing.T) {
a := nouvelAmontScripte(t, servir("état courant"))
p, _ := proxyEtCasier(t)
joue(t, p, "GET", a.hote(), "/depot/etat")
u, _ := url.Parse("http://" + a.hote() + "/depot/etat")
_, corps := p.Store.paths(CleDe("GET", u))
ancien := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
if err := os.Chtimes(corps, ancien, ancien); err != nil {
t.Fatal(err)
}
var sortie bytes.Buffer
EcrireDetentions(p.Store, strings.NewReader("GET "+u.String()+"\n"), &sortie)
if !strings.HasPrefix(sortie.String(), VerdictGarde+"\t") {
t.Fatalf("l'objet n'a pas été vu : %q", sortie.String())
}
fi, err := os.Stat(corps)
if err != nil {
t.Fatal(err)
}
if !fi.ModTime().Equal(ancien) {
t.Errorf("le relevé a rajeuni l'objet : %v au lieu de %v", fi.ModTime(), ancien)
}
}
// Un HEAD que le service ne gardera jamais est non-cachable, pas absent :
// l'appelant le croirait sinon à remplir, et le rejouerait pour rien. Le
// magasin est rempli ici PAR le service, l'amont répondant 200 à chaque
// HEAD : seul celui dont la classe garde un statut seul doit y entrer, et
// c'est le seul que « --detient » ne dit pas non-cachable.
func TestDetientUnHeadQueLeMagasinNeGardeJamais(t *testing.T) {
a := nouvelAmontScripte(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "1234")
w.WriteHeader(http.StatusOK)
})
p, dir := proxyEtCasier(t)
base := "http://" + a.hote()
attendus := map[string]string{
// Immuable et portable : un paquet.
"/core/os/x86_64/outil-1.0-1-x86_64.pkg.tar.zst": VerdictNonCachable,
// Immuable attaché à son hôte : un index par empreinte.
"/debian/dists/trixie/by-hash/SHA256/" + strings.Repeat("ab", 32): VerdictNonCachable,
// Volatile mais portable : un index de dépôt.
"/arch/core/os/x86_64/core.db": VerdictNonCachable,
// Volatile attaché à son hôte : le seul qui se garde.
"/api/bonjour": VerdictStatut,
}
for chemin, attendu := range attendus {
if got := p.Store.Detenir("HEAD", base+chemin).Verdict; got != VerdictNonCachable &&
got != VerdictAbsent {
t.Errorf("%s avant remplissage : %q", chemin, got)
}
joue(t, p, "HEAD", a.hote(), chemin)
if got := p.Store.Detenir("HEAD", base+chemin).Verdict; got != attendu {
t.Errorf("HEAD %s : %q, attendu %q", chemin, got, attendu)
}
// Le GET de la même URL reste à remplir : un corps s'y garde.
if got := p.Store.Detenir("GET", base+chemin).Verdict; got != VerdictAbsent {
t.Errorf("GET %s : %q, attendu %q", chemin, got, VerdictAbsent)
}
}
if n := corpsGardes(t, dir); n != 1 {
t.Errorf("%d objet(s) gardé(s), attendu 1 : le service et --detient"+
" divergent sur ce qu'un HEAD laisse au magasin", n)
}
if got := p.Store.Detenir("HEAD", base+"/api/jamais-vu").Verdict; got != VerdictAbsent {
t.Errorf("HEAD volatile jamais vu : %q, attendu %q", got, VerdictAbsent)
}
}
// Un corps dont la taille ne correspond plus à son méta n'est pas en
// réserve : le service refuserait de le servir.
func TestDetientRefuseUnCorpsAltere(t *testing.T) {
a := nouvelAmontScripte(t, servir("état courant"))
p, _ := proxyEtCasier(t)
joue(t, p, "GET", a.hote(), "/depot/etat")
u, _ := url.Parse("http://" + a.hote() + "/depot/etat")
_, corps := p.Store.paths(CleDe("GET", u))
if err := os.WriteFile(corps, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if d := p.Store.Detenir("GET", u.String()); d.Verdict != VerdictAbsent {
t.Errorf("verdict %q pour un corps altéré", d.Verdict)
}
EcrireDetentions(p.Store, strings.NewReader(""), io.Discard)
}