233 lines
6.3 KiB
Go
233 lines
6.3 KiB
Go
|
|
// © 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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|