erplibre/script/qemu_cache/variante_test.go

201 lines
6.3 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 (
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"sync"
"testing"
"time"
)
// Les deux en-têtes Accept d'un client de registre : la fiche abrégée, et la
// fiche complète.
const (
acceptAbrege = "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*"
acceptComplet = "application/json"
)
// amontAVariantes sert deux représentations d'une même URL selon Accept, chacune
// sous son ETag, avec « Vary: Accept » ; « 304 » à qui présente l'ETag de la
// représentation qu'il demande. Il compte les corps envoyés par représentation.
type amontAVariantes struct {
srv *httptest.Server
mu sync.Mutex
vary bool
corps map[string]int
}
func nouvelAmontAVariantes(t *testing.T, vary bool) *amontAVariantes {
t.Helper()
a := &amontAVariantes{vary: vary, corps: map[string]int{}}
a.srv = httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
a.mu.Lock()
defer a.mu.Unlock()
rep, etag, corps := "complète", `"complet"`, "fiche complète, très longue"
if strings.Contains(r.Header.Get("Accept"), "install-v1") {
rep, etag, corps = "abrégée", `"abrege"`, "fiche abrégée"
}
if a.vary {
w.Header().Set("Vary", "accept-encoding, accept")
}
w.Header().Set("ETag", etag)
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
a.corps[rep]++
io.WriteString(w, corps)
}))
t.Cleanup(a.srv.Close)
return a
}
func (a *amontAVariantes) envoyes(rep string) int {
a.mu.Lock()
defer a.mu.Unlock()
return a.corps[rep]
}
func (a *amontAVariantes) hote() string {
u, _ := url.Parse(a.srv.URL)
return u.Host
}
const ficheNpm = "/npm"
func demandeAccept(t *testing.T, p *Proxy, hote, accept string) *httptest.ResponseRecorder {
t.Helper()
r := httptest.NewRequest("GET", ficheNpm, nil)
r.Host = hote
if accept != "" {
r.Header.Set("Accept", accept)
}
w := httptest.NewRecorder()
p.serve(w, r, "http")
return w
}
// Deux représentations demandées tour à tour ne repartent qu'une fois chacune :
// rangée à part, l'une ne remplace plus l'autre.
func TestDeuxRepresentationsQuiAlternentNeRepartentQuUneFois(t *testing.T) {
a := nouvelAmontAVariantes(t, true)
u := &url.URL{Scheme: "http", Host: a.hote(), Path: ficheNpm}
if Classify(u) != ClassVolatile || PortableParChemin(u) {
t.Fatalf("%s n'est plus un volatil rangé avec son hôte", ficheNpm)
}
p := proxyDeTest(t)
ordre := []string{acceptAbrege, acceptComplet, acceptAbrege, acceptComplet, acceptAbrege}
for i, accept := range ordre {
w := demandeAccept(t, p, a.hote(), accept)
veut := "fiche complète, très longue"
if accept == acceptAbrege {
veut = "fiche abrégée"
}
if w.Body.String() != veut {
t.Fatalf("demande %d : corps %q, attendu %q", i+1, w.Body.String(), veut)
}
}
if n := a.envoyes("abrégée"); n != 1 {
t.Errorf("fiche abrégée envoyée %d fois, attendu 1", n)
}
if n := a.envoyes("complète"); n != 1 {
t.Errorf("fiche complète envoyée %d fois, attendu 1", n)
}
}
// La clé de base reste tenue : --detient, le pré-vol et le rattrapage hors
// ligne ne connaissent pas l'en-tête Accept.
func TestLaCleDeBaseResteTenuePourDetient(t *testing.T) {
a := nouvelAmontAVariantes(t, true)
p := proxyDeTest(t)
demandeAccept(t, p, a.hote(), acceptAbrege)
d := p.Store.Detenir("GET", "http://"+a.hote()+ficheNpm)
if d.Verdict != VerdictGarde {
t.Errorf("--detient dit %q pour une fiche gardée", d.Verdict)
}
}
// Sans « Vary: Accept », rien n'est rangé à part : la réponse ne varie pas, et
// la dupliquer ne ferait que doubler le disque.
func TestSansVaryAcceptAucuneVariante(t *testing.T) {
a := nouvelAmontAVariantes(t, false)
p := proxyDeTest(t)
demandeAccept(t, p, a.hote(), acceptAbrege)
base := CleDe("GET", &url.URL{Scheme: "http", Host: a.hote(), Path: ficheNpm})
if p.Store.Detient(CleVariante(base, acceptAbrege)) {
t.Error("une variante a été rangée pour une réponse sans Vary: Accept")
}
if !p.Store.Detient(base) {
t.Error("la clé de base n'est plus écrite")
}
}
// Amont muet : le client reçoit SA représentation, pas la dernière rangée sous
// la base, qui peut être l'autre.
func TestHorsLigneLaRepresentationDuClientSort(t *testing.T) {
a := nouvelAmontAVariantes(t, true)
p := proxyDeTest(t)
hote := a.hote()
demandeAccept(t, p, hote, acceptAbrege)
demandeAccept(t, p, hote, acceptComplet) // la base porte désormais la complète
a.srv.Close()
w := demandeAccept(t, p, hote, acceptAbrege)
if w.Code != http.StatusOK || w.Body.String() != "fiche abrégée" {
t.Errorf("hors ligne : %d %q, attendu la fiche abrégée", w.Code, w.Body.String())
}
}
// Une requête sans Accept ne choisit aucune représentation : elle suit la clé
// de base, comme avant.
func TestSansAcceptLaBaseSeule(t *testing.T) {
if CleVariante("cle", "") != "" || CleVariante("cle", " ") != "" {
t.Error("un Accept vide produit une clé de variante")
}
if CleVariante("cle", "Application/JSON") != CleVariante("cle", "application/json") {
t.Error("la casse d'Accept change la variante")
}
if !varieSurAccept(http.Header{"Vary": {"accept-encoding, Accept"}}) ||
varieSurAccept(http.Header{"Vary": {"accept-encoding"}}) {
t.Error("lecture de Vary erronée")
}
}
// Servir une variante sert aussi l'objet de base : sa date d'usage suit, sans
// quoi un nettoyage par âge retirerait la base qu'on sert encore, et --detient
// la dirait absente.
func TestServirUneVarianteRajeunitLaBase(t *testing.T) {
a := nouvelAmontAVariantes(t, true)
p := proxyDeTest(t)
demandeAccept(t, p, a.hote(), acceptAbrege)
base := CleDe("GET", &url.URL{Scheme: "http", Host: a.hote(), Path: ficheNpm})
_, corpsBase := p.Store.paths(base)
vieux := time.Now().Add(-90 * 24 * time.Hour)
if err := os.Chtimes(corpsBase, vieux, vieux); err != nil {
t.Fatal(err)
}
w := demandeAccept(t, p, a.hote(), acceptAbrege)
if got := w.Header().Get("X-ERPLibre-Cache"); got != OutcomeRevalidated {
t.Fatalf("seconde demande servie « %s », attendu « %s »", got, OutcomeRevalidated)
}
fi, err := os.Stat(corpsBase)
if err != nil {
t.Fatal(err)
}
if time.Since(fi.ModTime()) > time.Hour {
t.Errorf("la base date encore du %s : son usage n'a pas été noté", fi.ModTime())
}
}