CI / build (push) Successful in 54s
A Go server that takes product uploads with hub upload tokens, checks them by t5de-api's pack rules, stores files by sha256, registers revisions with the hub (POST /api/v1/catalog/revisions), and serves files with immutable caching; files used only by adult revisions need an adult access token. Contract: t5de-hub's docs/products.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
148 lines
3.8 KiB
Go
148 lines
3.8 KiB
Go
package store
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func open(t *testing.T, dir string) *Store {
|
|
t.Helper()
|
|
s, err := Open(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { s.Close() })
|
|
return s
|
|
}
|
|
|
|
func hashOf(data string) string {
|
|
sum := sha256.Sum256([]byte(data))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func TestFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
s := open(t, dir)
|
|
|
|
hash, err := s.PutFile([]byte("hello"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hash != hashOf("hello") {
|
|
t.Fatalf("hash %s", hash)
|
|
}
|
|
want := filepath.Join(dir, "files", hash[:2], hash)
|
|
if s.Path(hash) != want {
|
|
t.Fatalf("path %s, want %s", s.Path(hash), want)
|
|
}
|
|
again, err := s.PutFile([]byte("hello"))
|
|
if err != nil || again != hash {
|
|
t.Fatalf("again: %s, %v", again, err)
|
|
}
|
|
f, err := s.Open(hash)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := io.ReadAll(f)
|
|
f.Close()
|
|
if string(data) != "hello" {
|
|
t.Fatalf("data %q", data)
|
|
}
|
|
// No temp files left beside it.
|
|
entries, _ := os.ReadDir(filepath.Dir(want))
|
|
if len(entries) != 1 {
|
|
t.Fatalf("%d entries in %s", len(entries), filepath.Dir(want))
|
|
}
|
|
|
|
if _, err := s.Open(hashOf("missing")); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("missing: %v", err)
|
|
}
|
|
for _, bad := range []string{"", "../catalog.db", hash[:63], "A" + hash[1:]} {
|
|
if _, err := s.Open(bad); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("%q: %v", bad, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRevisions(t *testing.T) {
|
|
dir := t.TempDir()
|
|
s := open(t, dir)
|
|
shared, general, adult := hashOf("shared"), hashOf("general"), hashOf("adult")
|
|
|
|
if hosted, _, err := s.FileAccess(shared); err != nil || hosted {
|
|
t.Fatalf("before: hosted %v, %v", hosted, err)
|
|
}
|
|
|
|
a := Revision{Product: "aaaaaaaaaa", Revision: 1, Rating: Adult, Files: []File{
|
|
{Name: "a.png", SHA256: adult, Size: 5}, {Name: "index.xml", SHA256: shared, Size: 6},
|
|
}}
|
|
if err := s.AddRevision(a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check := func(hash string, wantHosted, wantAdult bool) {
|
|
t.Helper()
|
|
hosted, adultOnly, err := s.FileAccess(hash)
|
|
if err != nil || hosted != wantHosted || adultOnly != wantAdult {
|
|
t.Fatalf("%s: hosted %v adult %v (%v), want %v %v", hash[:8], hosted, adultOnly, err, wantHosted, wantAdult)
|
|
}
|
|
}
|
|
check(shared, true, true)
|
|
check(adult, true, true)
|
|
check(general, false, false)
|
|
|
|
g := Revision{Product: "bbbbbbbbbb", Revision: 2, Rating: General, Files: []File{
|
|
{Name: "g.png", SHA256: general, Size: 7}, {Name: "index.xml", SHA256: shared, Size: 6},
|
|
}}
|
|
if err := s.AddRevision(g); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check(shared, true, false) // also in a general revision
|
|
check(adult, true, true)
|
|
check(general, true, false)
|
|
|
|
got, err := s.Revision("aaaaaaaaaa", 1)
|
|
if err != nil || !reflect.DeepEqual(*got, a) {
|
|
t.Fatalf("got %+v, %v", got, err)
|
|
}
|
|
if _, err := s.Revision("aaaaaaaaaa", 2); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("missing revision: %v", err)
|
|
}
|
|
if err := s.AddRevision(Revision{Product: "c", Revision: 1, Rating: "GA"}); err == nil {
|
|
t.Fatal("bad rating accepted")
|
|
}
|
|
|
|
// Recording a revision again replaces it, and the data survives reopening.
|
|
a.Rating = General
|
|
if err := s.AddRevision(a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Close()
|
|
s = open(t, dir)
|
|
got, err = s.Revision("aaaaaaaaaa", 1)
|
|
if err != nil || got.Rating != General {
|
|
t.Fatalf("after reopening: %+v, %v", got, err)
|
|
}
|
|
if hosted, adultOnly, _ := s.FileAccess(adult); !hosted || adultOnly {
|
|
t.Fatal("adult file still adult-only after the revision became general")
|
|
}
|
|
}
|
|
|
|
func TestTempCleared(t *testing.T) {
|
|
dir := t.TempDir()
|
|
s := open(t, dir)
|
|
if err := os.WriteFile(filepath.Join(s.TempDir(), "leftover"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Close()
|
|
s = open(t, dir)
|
|
if entries, _ := os.ReadDir(s.TempDir()); len(entries) != 0 {
|
|
t.Fatalf("temp folder not emptied: %v", entries)
|
|
}
|
|
}
|