erplibre/script/qemu_cache/store_test.go

233 lines
6.3 KiB
Go
Raw Normal View History

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
package main
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
)
func ecrire(t *testing.T, s *Store, key, corps string, taille int64) error {
t.Helper()
w, err := s.NewWriter(key, Meta{
URL: "https://exemple.example/x", Method: "GET", Status: 200,
Header: http.Header{"Content-Type": []string{"application/octet-stream"}},
Class: ClassImmutable.String(),
})
if err != nil {
t.Fatalf("ouverture : %v", err)
}
io.WriteString(w, corps)
return w.Commit(taille)
}
func TestStorePuisGet(t *testing.T) {
s := &Store{Dir: t.TempDir()}
key := Key("GET", "https://exemple.example/x")
if err := ecrire(t, s, key, "bonjour", 7); err != nil {
t.Fatalf("commit : %v", err)
}
m, f, err := s.Get(key)
if err != nil {
t.Fatalf("get : %v", err)
}
defer f.Close()
if m.Size != 7 {
t.Errorf("taille %d, attendu 7", m.Size)
}
raw, _ := io.ReadAll(f)
if string(raw) != "bonjour" {
t.Errorf("corps %q, attendu %q", raw, "bonjour")
}
if m.StoredAt.IsZero() {
t.Error("la date de stockage manque")
}
}
// Une réponse tronquée par une coupure réseau ressemble à une réponse
// complète pour tout le reste du code : la taille annoncée est donc vérifiée.
func TestCorpsTronqueRefuse(t *testing.T) {
s := &Store{Dir: t.TempDir()}
key := Key("GET", "https://exemple.example/tronque")
err := ecrire(t, s, key, "court", 4096)
if err == nil {
t.Fatal("un corps tronqué a été accepté")
}
if !strings.Contains(err.Error(), "tronqué") {
t.Errorf("erreur %q, attendu qu'elle dise « tronqué »", err)
}
if _, _, err := s.Get(key); err == nil {
t.Error("l'objet refusé est pourtant lisible")
}
}
// Une taille inconnue à l'amont — pas de Content-Length — vaut -1 et ne
// déclenche aucune vérification.
func TestTailleInconnueAcceptee(t *testing.T) {
s := &Store{Dir: t.TempDir()}
key := Key("GET", "https://exemple.example/inconnue")
if err := ecrire(t, s, key, "quoi que ce soit", -1); err != nil {
t.Fatalf("commit : %v", err)
}
if _, f, err := s.Get(key); err != nil {
t.Errorf("get : %v", err)
} else {
f.Close()
}
}
// Un objet dont le corps ne correspond plus aux métadonnées vaut absent
// plutôt que faux.
func TestCorpsAlteréVautAbsent(t *testing.T) {
s := &Store{Dir: t.TempDir()}
key := Key("GET", "https://exemple.example/altere")
if err := ecrire(t, s, key, "bonjour", 7); err != nil {
t.Fatalf("commit : %v", err)
}
_, body := s.paths(key)
if err := os.WriteFile(body, []byte("plus court"), 0o644); err != nil {
t.Fatalf("altération : %v", err)
}
if _, _, err := s.Get(key); err == nil {
t.Error("un corps de taille inattendue a été servi")
}
}
func TestAbortNeLaisseRien(t *testing.T) {
dir := t.TempDir()
s := &Store{Dir: dir}
key := Key("GET", "https://exemple.example/abandon")
w, err := s.NewWriter(key, Meta{URL: "x", Method: "GET", Status: 200})
if err != nil {
t.Fatalf("ouverture : %v", err)
}
io.WriteString(w, "à moitié")
w.Abort()
if _, _, err := s.Get(key); err == nil {
t.Error("un objet abandonné est lisible")
}
var restes []string
filepath.Walk(dir, func(p string, info os.FileInfo, err error) error {
if err == nil && !info.IsDir() {
restes = append(restes, p)
}
return nil
})
if len(restes) != 0 {
t.Errorf("l'abandon laisse %v", restes)
}
}
func TestSweepPartials(t *testing.T) {
dir := t.TempDir()
s := &Store{Dir: dir}
sub := filepath.Join(dir, "ab", "cd")
os.MkdirAll(sub, 0o755)
os.WriteFile(filepath.Join(sub, "clef.part-123"), []byte("x"), 0o644)
os.WriteFile(filepath.Join(sub, "clef.body"), []byte("x"), 0o644)
if n := s.SweepPartials(); n != 1 {
t.Errorf("%d fichier(s) balayé(s), attendu 1", n)
}
if _, err := os.Stat(filepath.Join(sub, "clef.body")); err != nil {
t.Error("le balayage a emporté un corps complet")
}
}
func TestStat(t *testing.T) {
s := &Store{Dir: t.TempDir()}
for _, u := range []string{"a", "b", "c"} {
if err := ecrire(t, s, Key("GET", u), "12345", 5); err != nil {
t.Fatalf("commit %s : %v", u, err)
}
}
st, err := s.Stat()
if err != nil {
t.Fatalf("stat : %v", err)
}
if st.Objects != 3 {
t.Errorf("%d objets, attendu 3", st.Objects)
}
if st.Bytes != 15 {
t.Errorf("%d octets, attendu 15", st.Bytes)
}
if st.Oldest.IsZero() {
t.Error("aucune date pour le plus ancien objet")
}
}
// Un répertoire de cache qui n'existe pas encore rend un état vide et non une
// erreur : « --status » doit répondre avant le premier démarrage.
func TestStatSansRepertoire(t *testing.T) {
s := &Store{Dir: filepath.Join(t.TempDir(), "jamais-cree")}
st, err := s.Stat()
if err != nil {
t.Fatalf("stat : %v", err)
}
if st.Objects != 0 || st.Bytes != 0 {
t.Errorf("état %+v, attendu vide", st)
}
}
func TestKeyDistingueMethode(t *testing.T) {
if Key("GET", "https://x/y") == Key("HEAD", "https://x/y") {
t.Error("GET et HEAD partagent une clé : un HEAD servirait un corps")
}
}
// Detient veut dire « tient un corps 200 ». Un statut seul n'en est pas un,
// quel que soit ce que son fichier de corps contient ; un méta sans statut,
// écrit avant que le magasin garde autre chose que des corps, en est un.
func TestDetientNeCompteQueLesCorps200(t *testing.T) {
s := &Store{Dir: t.TempDir()}
cas := []struct {
statut int
seul bool
attendu bool
}{
{http.StatusFound, false, false},
{http.StatusNotFound, false, false},
// Le 200 d'un HEAD : un statut seul malgré son code.
{http.StatusOK, true, false},
{http.StatusOK, false, true},
{0, false, true},
}
for i, c := range cas {
key := Key("GET", fmt.Sprintf("https://exemple.example/%d", i))
w, err := s.NewWriter(key, Meta{
URL: "x", Method: "GET", Status: c.statut, StatusOnly: c.seul,
})
if err != nil {
t.Fatal(err)
}
io.WriteString(w, "corps")
if err := w.Commit(5); err != nil {
t.Fatal(err)
}
if got := s.Detient(key); got != c.attendu {
t.Errorf("statut %d : Detient %v, attendu %v", c.statut, got, c.attendu)
}
}
}
func TestHumanBytes(t *testing.T) {
cas := map[int64]string{
512: "512 o",
2048: "2.0 Kio",
5 * 1024 * 1024: "5.0 Mio",
3 * 1024 * 1024 * 1024: "3.0 Gio",
}
for n, attendu := range cas {
if got := HumanBytes(n); got != attendu {
t.Errorf("%d octets rendus %q, attendu %q", n, got, attendu)
}
}
}