511 lines
17 KiB
Go
511 lines
17 KiB
Go
|
|
// © 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"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"sync/atomic"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
// amont monte un serveur qui compte ce qu'on lui demande. Le compteur EST la
|
||
|
|
// mesure de tous ces tests : le cache n'a d'intérêt que s'il fait baisser le
|
||
|
|
// nombre de requêtes qui sortent.
|
||
|
|
type amont struct {
|
||
|
|
srv *httptest.Server
|
||
|
|
appel int64
|
||
|
|
corps string
|
||
|
|
}
|
||
|
|
|
||
|
|
func nouvelAmont(t *testing.T, corps string) *amont {
|
||
|
|
t.Helper()
|
||
|
|
a := &amont{corps: corps}
|
||
|
|
a.srv = httptest.NewServer(http.HandlerFunc(
|
||
|
|
func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
atomic.AddInt64(&a.appel, 1)
|
||
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||
|
|
io.WriteString(w, a.corps)
|
||
|
|
}))
|
||
|
|
t.Cleanup(a.srv.Close)
|
||
|
|
return a
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *amont) appels() int64 { return atomic.LoadInt64(&a.appel) }
|
||
|
|
|
||
|
|
func (a *amont) hote() string {
|
||
|
|
u, _ := url.Parse(a.srv.URL)
|
||
|
|
return u.Host
|
||
|
|
}
|
||
|
|
|
||
|
|
func proxyDeTest(t *testing.T) *Proxy {
|
||
|
|
t.Helper()
|
||
|
|
alog, err := OpenAccessLog("")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("journal : %v", err)
|
||
|
|
}
|
||
|
|
return NewProxy(&Store{Dir: t.TempDir()}, alog)
|
||
|
|
}
|
||
|
|
|
||
|
|
// demande joue une requête telle que l'interception transparente la présente :
|
||
|
|
// une ligne de requête sans hôte, l'hôte venant de l'en-tête.
|
||
|
|
func demande(t *testing.T, p *Proxy, hote, chemin string) *httptest.ResponseRecorder {
|
||
|
|
t.Helper()
|
||
|
|
r := httptest.NewRequest("GET", chemin, nil)
|
||
|
|
r.Host = hote
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
p.serve(w, r, "http")
|
||
|
|
return w
|
||
|
|
}
|
||
|
|
|
||
|
|
// Invariant 1 : un index n'est JAMAIS servi du cache tant que l'amont répond.
|
||
|
|
//
|
||
|
|
// C'est la règle qui empêche le cache de provoquer le « failed retrieving
|
||
|
|
// file … 404 » sur un paquet retiré des miroirs : servir un index périmé
|
||
|
|
// promet des fichiers qui n'existent plus.
|
||
|
|
func TestIndexJamaisServiQuandAmontRepond(t *testing.T) {
|
||
|
|
a := nouvelAmont(t, "base de paquets")
|
||
|
|
p := proxyDeTest(t)
|
||
|
|
|
||
|
|
for i := 1; i <= 3; i++ {
|
||
|
|
w := demande(t, p, a.hote(), "/arch/core/os/x86_64/core.db")
|
||
|
|
if w.Code != 200 {
|
||
|
|
t.Fatalf("requête %d : code %d", i, w.Code)
|
||
|
|
}
|
||
|
|
if got := w.Header().Get("X-ERPLibre-Cache"); got != "miss" {
|
||
|
|
t.Errorf("requête %d : servie « %s », attendu « miss »", i, got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if n := a.appels(); n != 3 {
|
||
|
|
t.Errorf("l'amont a reçu %d requêtes, attendu 3 : un index a été servi du cache", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Invariant 2 : un fichier de paquet est servi du cache dès la seconde
|
||
|
|
// demande. C'est le gain que l'outil existe pour produire.
|
||
|
|
func TestPaquetServiDuCache(t *testing.T) {
|
||
|
|
a := nouvelAmont(t, "contenu du paquet")
|
||
|
|
p := proxyDeTest(t)
|
||
|
|
chemin := "/arch/core/os/x86_64/bash-5.2-1-x86_64.pkg.tar.zst"
|
||
|
|
|
||
|
|
w1 := demande(t, p, a.hote(), chemin)
|
||
|
|
if w1.Header().Get("X-ERPLibre-Cache") != "miss" {
|
||
|
|
t.Fatalf("première demande servie du cache : %s",
|
||
|
|
w1.Header().Get("X-ERPLibre-Cache"))
|
||
|
|
}
|
||
|
|
w2 := demande(t, p, a.hote(), chemin)
|
||
|
|
if got := w2.Header().Get("X-ERPLibre-Cache"); got != OutcomeHit {
|
||
|
|
t.Errorf("seconde demande servie « %s », attendu « %s »", got, OutcomeHit)
|
||
|
|
}
|
||
|
|
if w2.Body.String() != a.corps {
|
||
|
|
t.Errorf("corps servi %q, attendu %q", w2.Body.String(), a.corps)
|
||
|
|
}
|
||
|
|
if n := a.appels(); n != 1 {
|
||
|
|
t.Errorf("l'amont a reçu %d requêtes, attendu 1", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Invariant 3 : l'amont muet fait sortir la copie stockée, index compris.
|
||
|
|
// C'est ce qui rend un déploiement possible sans réseau du tout.
|
||
|
|
func TestIndexServiQuandAmontMuet(t *testing.T) {
|
||
|
|
a := nouvelAmont(t, "base de paquets")
|
||
|
|
p := proxyDeTest(t)
|
||
|
|
chemin := "/arch/core/os/x86_64/core.db"
|
||
|
|
|
||
|
|
if w := demande(t, p, a.hote(), chemin); w.Code != 200 {
|
||
|
|
t.Fatalf("remplissage : code %d", w.Code)
|
||
|
|
}
|
||
|
|
hote := a.hote()
|
||
|
|
a.srv.Close() // l'amont devient injoignable
|
||
|
|
|
||
|
|
w := demande(t, p, hote, chemin)
|
||
|
|
if w.Code != 200 {
|
||
|
|
t.Fatalf("hors ligne : code %d, attendu 200", w.Code)
|
||
|
|
}
|
||
|
|
if got := w.Header().Get("X-ERPLibre-Cache"); got != OutcomeStale {
|
||
|
|
t.Errorf("servi « %s », attendu « %s »", got, OutcomeStale)
|
||
|
|
}
|
||
|
|
if w.Header().Get("X-ERPLibre-Cache-Date") == "" {
|
||
|
|
t.Error("la date de l'instantané manque : l'opérateur ne peut pas savoir de quand datent ses octets")
|
||
|
|
}
|
||
|
|
if w.Body.String() != a.corps {
|
||
|
|
t.Errorf("corps servi %q, attendu %q", w.Body.String(), a.corps)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Invariant 4 : hors ligne et sans copie, l'erreur NOMME le fichier.
|
||
|
|
//
|
||
|
|
// Un 404 nu ferait accuser le miroir : le client n'a aucun moyen de savoir
|
||
|
|
// qu'un cache s'est interposé.
|
||
|
|
func TestDefautHorsLigneNommeLeFichier(t *testing.T) {
|
||
|
|
a := nouvelAmont(t, "peu importe")
|
||
|
|
hote := a.hote()
|
||
|
|
a.srv.Close()
|
||
|
|
p := proxyDeTest(t)
|
||
|
|
|
||
|
|
w := demande(t, p, hote, "/arch/core/os/x86_64/jamais-vu.pkg.tar.zst")
|
||
|
|
if w.Code != http.StatusGatewayTimeout {
|
||
|
|
t.Fatalf("code %d, attendu %d", w.Code, http.StatusGatewayTimeout)
|
||
|
|
}
|
||
|
|
corps := w.Body.String()
|
||
|
|
for _, attendu := range []string{"jamais-vu.pkg.tar.zst", hote, "erplibre_go_qemu_cache"} {
|
||
|
|
if !strings.Contains(corps, attendu) {
|
||
|
|
t.Errorf("le message ne dit pas %q :\n%s", attendu, corps)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une requête partielle ne remplit pas le cache : un fragment ne sert à rien
|
||
|
|
// à la demande suivante, et le garder comme un corps entier servirait un
|
||
|
|
// paquet tronqué.
|
||
|
|
func TestRequetePartielleNonGardee(t *testing.T) {
|
||
|
|
a := nouvelAmont(t, "0123456789")
|
||
|
|
p := proxyDeTest(t)
|
||
|
|
chemin := "/x/paquet-1-1-x86_64.pkg.tar.zst"
|
||
|
|
|
||
|
|
r := httptest.NewRequest("GET", chemin, nil)
|
||
|
|
r.Host = a.hote()
|
||
|
|
r.Header.Set("Range", "bytes=0-4")
|
||
|
|
p.serve(httptest.NewRecorder(), r, "http")
|
||
|
|
|
||
|
|
// Une demande entière ensuite doit ressortir à l'amont.
|
||
|
|
w := demande(t, p, a.hote(), chemin)
|
||
|
|
if got := w.Header().Get("X-ERPLibre-Cache"); got == OutcomeHit {
|
||
|
|
t.Error("un fragment est entré au cache et a été servi comme un corps entier")
|
||
|
|
}
|
||
|
|
if n := a.appels(); n != 2 {
|
||
|
|
t.Errorf("l'amont a reçu %d requêtes, attendu 2", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un POST ne se cache pas : il change un état à l'amont.
|
||
|
|
func TestPostNonCache(t *testing.T) {
|
||
|
|
a := nouvelAmont(t, "réponse")
|
||
|
|
p := proxyDeTest(t)
|
||
|
|
|
||
|
|
for i := 0; i < 2; i++ {
|
||
|
|
r := httptest.NewRequest("POST", "/api/quelque-chose.whl", strings.NewReader("x"))
|
||
|
|
r.Host = a.hote()
|
||
|
|
p.serve(httptest.NewRecorder(), r, "http")
|
||
|
|
}
|
||
|
|
if n := a.appels(); n != 2 {
|
||
|
|
t.Errorf("l'amont a reçu %d requêtes, attendu 2 : un POST a été caché", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un transport qui note le client par lequel la requête est passée.
|
||
|
|
type transportTemoin struct {
|
||
|
|
nom string
|
||
|
|
vues *[]string
|
||
|
|
}
|
||
|
|
|
||
|
|
func (t transportTemoin) RoundTrip(r *http.Request) (*http.Response, error) {
|
||
|
|
*t.vues = append(*t.vues, t.nom)
|
||
|
|
return &http.Response{
|
||
|
|
StatusCode: 200,
|
||
|
|
Header: make(http.Header),
|
||
|
|
Body: io.NopCloser(strings.NewReader("")),
|
||
|
|
Request: r,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Le délai court existe pour que le repli hors ligne arrive AVANT que le
|
||
|
|
// client renonce. Il prenait le calcul d'un serveur git pour un amont
|
||
|
|
// injoignable — mesuré à seize secondes avant le premier octet — et rendait un
|
||
|
|
// 504 sur un dépôt parfaitement joignable, faisant échouer « repo sync ».
|
||
|
|
func TestGitPasseParLeClientPatient(t *testing.T) {
|
||
|
|
var vues []string
|
||
|
|
p := &Proxy{
|
||
|
|
Client: &http.Client{
|
||
|
|
Transport: transportTemoin{"court", &vues},
|
||
|
|
},
|
||
|
|
ClientPatient: &http.Client{
|
||
|
|
Transport: transportTemoin{"patient", &vues},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
cas := []struct {
|
||
|
|
brut string
|
||
|
|
attendu string
|
||
|
|
}{
|
||
|
|
{"https://git.example/o/d.git/info/refs?service=git-upload-pack",
|
||
|
|
"patient"},
|
||
|
|
{"https://git.example/o/d.git/git-upload-pack", "patient"},
|
||
|
|
{"https://miroir.example/core/os/x86_64/bash-5.2-1-x86_64.pkg.tar.zst",
|
||
|
|
"court"},
|
||
|
|
{"https://miroir.example/core/os/x86_64/core.db", "court"},
|
||
|
|
}
|
||
|
|
for _, c := range cas {
|
||
|
|
vues = nil
|
||
|
|
u, _ := url.Parse(c.brut)
|
||
|
|
r, _ := http.NewRequest("GET", c.brut, nil)
|
||
|
|
if _, err := p.fetch(r, u, false); err != nil {
|
||
|
|
t.Fatalf("%s : %v", c.brut, err)
|
||
|
|
}
|
||
|
|
if len(vues) != 1 || vues[0] != c.attendu {
|
||
|
|
t.Errorf("%s passé par %v, attendu « %s »",
|
||
|
|
c.brut, vues, c.attendu)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Les deux clients ne se distinguent que par leur patience : un amont
|
||
|
|
// injoignable reste détecté à l'établissement, en quatre secondes, dans les
|
||
|
|
// deux cas.
|
||
|
|
func TestLeClientPatientLestUniquementSurLesEnTetes(t *testing.T) {
|
||
|
|
p := NewProxy(&Store{Dir: t.TempDir()}, nil)
|
||
|
|
court := p.Client.Transport.(*http.Transport)
|
||
|
|
patient := p.ClientPatient.Transport.(*http.Transport)
|
||
|
|
if patient.ResponseHeaderTimeout <= court.ResponseHeaderTimeout {
|
||
|
|
t.Errorf("le client patient n'attend pas plus : %v contre %v",
|
||
|
|
patient.ResponseHeaderTimeout, court.ResponseHeaderTimeout)
|
||
|
|
}
|
||
|
|
if patient.TLSHandshakeTimeout != court.TLSHandshakeTimeout {
|
||
|
|
t.Error("le délai de poignée de main ne doit pas changer")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un paquet publié derrière une redirection vers une URL SIGNÉE.
|
||
|
|
//
|
||
|
|
// La cible change à chaque requête — c'est ce que fait une signature à
|
||
|
|
// péremption. Rendre la redirection au client, en comptant qu'il la
|
||
|
|
// redemandera au travers du cache, suppose une cible stable : ici la machine
|
||
|
|
// suivante ne retrouve rien et retélécharge le paquet en entier.
|
||
|
|
func TestPaquetDerriereUneRedirectionSignee(t *testing.T) {
|
||
|
|
var telechargements, signature int64
|
||
|
|
contenu := strings.Repeat("charge utile", 64)
|
||
|
|
|
||
|
|
stockage := httptest.NewServer(http.HandlerFunc(
|
||
|
|
func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
atomic.AddInt64(&telechargements, 1)
|
||
|
|
io.WriteString(w, contenu)
|
||
|
|
}))
|
||
|
|
t.Cleanup(stockage.Close)
|
||
|
|
|
||
|
|
publication := httptest.NewServer(http.HandlerFunc(
|
||
|
|
func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
n := atomic.AddInt64(&signature, 1)
|
||
|
|
http.Redirect(w, r,
|
||
|
|
stockage.URL+"/objet?signature="+string(rune('a'+n)),
|
||
|
|
http.StatusFound)
|
||
|
|
}))
|
||
|
|
t.Cleanup(publication.Close)
|
||
|
|
|
||
|
|
u, _ := url.Parse(publication.URL)
|
||
|
|
p := proxyDeTest(t)
|
||
|
|
chemin := "/paquets/releases/download/1.0/outil_1.0_amd64.deb"
|
||
|
|
|
||
|
|
for i := 1; i <= 3; i++ {
|
||
|
|
w := demande(t, p, u.Host, chemin)
|
||
|
|
if w.Code != 200 {
|
||
|
|
t.Fatalf("requête %d : code %d, attendu 200", i, w.Code)
|
||
|
|
}
|
||
|
|
if w.Body.String() != contenu {
|
||
|
|
t.Fatalf("requête %d : corps de %d octets", i, w.Body.Len())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if n := atomic.LoadInt64(&telechargements); n != 1 {
|
||
|
|
t.Errorf("le paquet est descendu %d fois, attendu 1 :"+
|
||
|
|
" la redirection empêche le cache de servir", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un index, lui, garde l'ancien comportement : la redirection lui est rendue,
|
||
|
|
// et il la redemandera. Rien n'est gardé d'un index tant que l'amont répond,
|
||
|
|
// donc suivre la chaîne n'apporterait rien et masquerait au client l'hôte qui
|
||
|
|
// lui a réellement répondu.
|
||
|
|
func TestUnIndexRecoitSaRedirection(t *testing.T) {
|
||
|
|
cible := httptest.NewServer(http.HandlerFunc(
|
||
|
|
func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
io.WriteString(w, "base")
|
||
|
|
}))
|
||
|
|
t.Cleanup(cible.Close)
|
||
|
|
publication := httptest.NewServer(http.HandlerFunc(
|
||
|
|
func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
http.Redirect(w, r, cible.URL+"/vraie.db", http.StatusFound)
|
||
|
|
}))
|
||
|
|
t.Cleanup(publication.Close)
|
||
|
|
|
||
|
|
u, _ := url.Parse(publication.URL)
|
||
|
|
w := demande(t, proxyDeTest(t), u.Host, "/arch/core/os/x86_64/core.db")
|
||
|
|
if w.Code != http.StatusFound {
|
||
|
|
t.Errorf("code %d, attendu 302 : la redirection a été suivie", w.Code)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une boucle de redirections doit rendre la main plutôt que tourner.
|
||
|
|
func TestUneBoucleDeRedirectionsSarrete(t *testing.T) {
|
||
|
|
var vues int64
|
||
|
|
var srv *httptest.Server
|
||
|
|
srv = httptest.NewServer(http.HandlerFunc(
|
||
|
|
func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
atomic.AddInt64(&vues, 1)
|
||
|
|
http.Redirect(w, r, srv.URL+"/tourne.deb", http.StatusFound)
|
||
|
|
}))
|
||
|
|
t.Cleanup(srv.Close)
|
||
|
|
|
||
|
|
u, _ := url.Parse(srv.URL)
|
||
|
|
w := demande(t, proxyDeTest(t), u.Host, "/tourne.deb")
|
||
|
|
if w.Code != http.StatusFound {
|
||
|
|
t.Errorf("code %d, attendu 302 au bout de la chaîne", w.Code)
|
||
|
|
}
|
||
|
|
if n := atomic.LoadInt64(&vues); n > maxRedirections+1 {
|
||
|
|
t.Errorf("%d requêtes pour une boucle, borne %d",
|
||
|
|
n, maxRedirections+1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// proxyEtCasier rend un proxy et le répertoire de son casier : les contrôles
|
||
|
|
// qui suivent ont besoin de constater ce qui a été GARDÉ, pas seulement ce
|
||
|
|
// qui a été rendu.
|
||
|
|
func proxyEtCasier(t *testing.T) (*Proxy, string) {
|
||
|
|
t.Helper()
|
||
|
|
alog, err := OpenAccessLog("")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("journal : %v", err)
|
||
|
|
}
|
||
|
|
dir := t.TempDir()
|
||
|
|
return NewProxy(&Store{Dir: dir}, alog), dir
|
||
|
|
}
|
||
|
|
|
||
|
|
func corpsGardes(t *testing.T, dir string) int {
|
||
|
|
t.Helper()
|
||
|
|
n := 0
|
||
|
|
_ = filepath.Walk(dir, func(p string, fi os.FileInfo, err error) error {
|
||
|
|
if err == nil && !fi.IsDir() && strings.HasSuffix(p, ".body") {
|
||
|
|
n++
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
return n
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une image cloud livre déjà l'index de sa suite de base. Son apt le REVALIDE
|
||
|
|
// au lieu de le télécharger, l'amont rend « 304 », et le cache n'a donc
|
||
|
|
// jamais de corps à garder pour cette ressource — aussi longtemps que ses
|
||
|
|
// clients en détiennent une copie, c'est-à-dire toujours.
|
||
|
|
//
|
||
|
|
// Hors ligne, la suite de base est alors la seule à manquer : ses voisines
|
||
|
|
// « -updates », « -security » et « -backports » sortent périmées du cache, et
|
||
|
|
// apt ne trouve plus un paquet de la base pendant que le reste marche.
|
||
|
|
func TestUneConditionnelleNeLaissePasLeCasierVide(t *testing.T) {
|
||
|
|
var recues []string
|
||
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
||
|
|
func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
recues = append(recues, r.Header.Get("If-Modified-Since"))
|
||
|
|
if r.Header.Get("If-Modified-Since") != "" {
|
||
|
|
w.WriteHeader(http.StatusNotModified)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
_, _ = w.Write([]byte("Origin: Ubuntu\nSuite: base\n"))
|
||
|
|
}))
|
||
|
|
defer srv.Close()
|
||
|
|
hote, _ := url.Parse(srv.URL)
|
||
|
|
|
||
|
|
p, dir := proxyEtCasier(t)
|
||
|
|
joue := func() *httptest.ResponseRecorder {
|
||
|
|
r := httptest.NewRequest("GET", "/ubuntu/dists/base/InRelease", nil)
|
||
|
|
r.Host = hote.Host
|
||
|
|
r.Header.Set("If-Modified-Since", "Wed, 01 Jan 2020 00:00:00 GMT")
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
p.serve(w, r, "http")
|
||
|
|
return w
|
||
|
|
}
|
||
|
|
|
||
|
|
if w := joue(); w.Code != http.StatusOK {
|
||
|
|
t.Fatalf("statut rendu %d, attendu 200", w.Code)
|
||
|
|
}
|
||
|
|
if len(recues) != 1 || recues[0] != "" {
|
||
|
|
t.Fatalf("la condition est partie à l'amont : %q", recues)
|
||
|
|
}
|
||
|
|
if corpsGardes(t, dir) == 0 {
|
||
|
|
t.Fatal("rien n'est gardé : le cache restera vide pour cette suite")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Le corps est là : la condition peut repartir, et un « 304 » ne coûte
|
||
|
|
// alors plus rien — c'est ce qui économise la bande passante.
|
||
|
|
recues = nil
|
||
|
|
joue()
|
||
|
|
if len(recues) != 1 || recues[0] == "" {
|
||
|
|
t.Fatalf("la condition a encore été retirée : %q", recues)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Amont muet et client qui détient déjà sa copie : lui rendre « 504 » le
|
||
|
|
// prive d'une suite entière qu'il pouvait lire. « 304 » le laisse garder la
|
||
|
|
// sienne, ce que « stale-if-error » veut dire pour une conditionnelle.
|
||
|
|
func TestAmontMuetEtClientQuiDetientDejaSaCopie(t *testing.T) {
|
||
|
|
p, _ := proxyEtCasier(t)
|
||
|
|
r := httptest.NewRequest("GET", "/ubuntu/dists/base/InRelease", nil)
|
||
|
|
// Rien n'écoute : l'amont est injoignable.
|
||
|
|
r.Host = "127.0.0.1:1"
|
||
|
|
r.Header.Set("If-None-Match", `"abc"`)
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
p.serve(w, r, "http")
|
||
|
|
|
||
|
|
if w.Code != http.StatusNotModified {
|
||
|
|
t.Fatalf("statut rendu %d, attendu 304", w.Code)
|
||
|
|
}
|
||
|
|
if got := w.Header().Get("X-ERPLibre-Cache"); got != OutcomeKeep {
|
||
|
|
t.Fatalf("issue %q, attendue %q", got, OutcomeKeep)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sans condition, le client n'a rien à garder : le 504 reste la réponse
|
||
|
|
// juste, et il dit pourquoi.
|
||
|
|
func TestAmontMuetSansCopieChezLeClient(t *testing.T) {
|
||
|
|
p, _ := proxyEtCasier(t)
|
||
|
|
r := httptest.NewRequest("GET", "/ubuntu/dists/base/InRelease", nil)
|
||
|
|
r.Host = "127.0.0.1:1"
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
p.serve(w, r, "http")
|
||
|
|
|
||
|
|
if w.Code != http.StatusGatewayTimeout {
|
||
|
|
t.Fatalf("statut rendu %d, attendu 504", w.Code)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Un corps d'erreur finit régulièrement dans un shell : « curl … | bash »
|
||
|
|
// est l'idiome de la moitié des installateurs, et « curl » sans « -f » lui
|
||
|
|
// livre le corps d'un 504 comme s'il l'avait demandé.
|
||
|
|
//
|
||
|
|
// Le message doit donc ne RIEN faire une fois exécuté. Sans cela, l'échec
|
||
|
|
// se lit « bash: line 1: erplibre_go_qemu_cache: command not found » et la
|
||
|
|
// cause véritable, elle, ne se lit plus.
|
||
|
|
func TestLeMessageHorsLigneEstInerteDansUnShell(t *testing.T) {
|
||
|
|
p, _ := proxyEtCasier(t)
|
||
|
|
r := httptest.NewRequest("GET", "/pyenv-installer", nil)
|
||
|
|
r.Host = "127.0.0.1:1"
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
p.serve(w, r, "http")
|
||
|
|
|
||
|
|
if w.Code != http.StatusGatewayTimeout {
|
||
|
|
t.Fatalf("statut rendu %d, attendu 504", w.Code)
|
||
|
|
}
|
||
|
|
for i, l := range strings.Split(strings.TrimRight(w.Body.String(), "\n"), "\n") {
|
||
|
|
if !strings.HasPrefix(l, "#") {
|
||
|
|
t.Fatalf("ligne %d exécutable par un shell : %q", i+1, l)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Inerte ne veut pas dire muet : la cause reste lisible.
|
||
|
|
if !strings.Contains(w.Body.String(), "amont injoignable") {
|
||
|
|
t.Fatal("le message ne dit plus pourquoi")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Une cause qui porte plusieurs lignes ne doit pas sortir du commentaire :
|
||
|
|
// il suffirait de la première pour rendre la suite exécutable.
|
||
|
|
func TestUneCauseMultiligneResteCommentee(t *testing.T) {
|
||
|
|
got := enCommentaire("premier\nseconde\ntroisième\n")
|
||
|
|
for _, l := range strings.Split(strings.TrimRight(got, "\n"), "\n") {
|
||
|
|
if !strings.HasPrefix(l, "# ") {
|
||
|
|
t.Fatalf("ligne non commentée : %q", l)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|