1752ef05a6
HTTP API + on-disk storage + auth-service token verification + dev mode. 31 tests pass, vet clean. See DESIGN.md for the architecture and README.md for the operator surface. Pending: pg-backed per-user quota override, snapshot retention / blob GC, tarball-vs-manifest content cross-check, end-to-end deploy on john.
194 lines
4.7 KiB
Go
194 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func newStorage(t *testing.T) *Storage {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
s, err := NewStorage(dir)
|
|
if err != nil {
|
|
t.Fatalf("NewStorage: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func buildManifest(id, sha string) *Manifest {
|
|
return &Manifest{
|
|
SnapshotID: id,
|
|
CreatedAt: time.Date(2026, 6, 2, 12, 0, 0, 0, time.UTC),
|
|
Files: map[string]FileEntry{
|
|
"options.txt": {
|
|
SHA256: sha,
|
|
Size: 5,
|
|
Mtime: time.Date(2026, 6, 2, 11, 30, 0, 0, time.UTC),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestStorage_RoundTripSnapshot(t *testing.T) {
|
|
s := newStorage(t)
|
|
|
|
sha, err := s.WriteBlob("123456789", []byte("hello"))
|
|
if err != nil {
|
|
t.Fatalf("WriteBlob: %v", err)
|
|
}
|
|
if sha != HashBytes([]byte("hello")) {
|
|
t.Fatalf("sha mismatch: got %s", sha)
|
|
}
|
|
|
|
m := buildManifest("01J9XQK4Z3DEMO0001", sha)
|
|
if err := s.StoreSnapshot("123456789", m, []byte("fake-tarball-bytes")); err != nil {
|
|
t.Fatalf("StoreSnapshot: %v", err)
|
|
}
|
|
|
|
got, err := s.ReadManifest("123456789")
|
|
if err != nil {
|
|
t.Fatalf("ReadManifest: %v", err)
|
|
}
|
|
if got.SnapshotID != "01J9XQK4Z3DEMO0001" {
|
|
t.Errorf("snapshot id mismatch: got %s", got.SnapshotID)
|
|
}
|
|
if _, ok := got.Files["options.txt"]; !ok {
|
|
t.Errorf("files key missing")
|
|
}
|
|
|
|
rc, err := s.ReadBlob("123456789", sha)
|
|
if err != nil {
|
|
t.Fatalf("ReadBlob: %v", err)
|
|
}
|
|
defer rc.Close()
|
|
data, _ := io.ReadAll(rc)
|
|
if string(data) != "hello" {
|
|
t.Errorf("blob content: got %q", string(data))
|
|
}
|
|
}
|
|
|
|
func TestStorage_BlobDedupe(t *testing.T) {
|
|
s := newStorage(t)
|
|
a, _ := s.WriteBlob("u1", []byte("xyz"))
|
|
b, _ := s.WriteBlob("u1", []byte("xyz"))
|
|
if a != b {
|
|
t.Errorf("dedupe broken: got %s vs %s", a, b)
|
|
}
|
|
// Both calls should leave one blob on disk
|
|
p := s.blobPath("u1", a)
|
|
info, err := os.Stat(p)
|
|
if err != nil {
|
|
t.Fatalf("stat blob: %v", err)
|
|
}
|
|
if info.Size() != 3 {
|
|
t.Errorf("blob size: got %d, want 3", info.Size())
|
|
}
|
|
}
|
|
|
|
func TestStorage_ReadManifest_MissingIsNotExist(t *testing.T) {
|
|
s := newStorage(t)
|
|
_, err := s.ReadManifest("never-stored")
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("expected os.ErrNotExist, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStorage_ListSnapshots_NewestFirst(t *testing.T) {
|
|
s := newStorage(t)
|
|
user := "u1"
|
|
sha, _ := s.WriteBlob(user, []byte("x"))
|
|
for _, id := range []string{"01ABC", "01BBB", "01ZZZ"} {
|
|
m := buildManifest(id, sha)
|
|
if err := s.StoreSnapshot(user, m, []byte("tar")); err != nil {
|
|
t.Fatalf("StoreSnapshot %s: %v", id, err)
|
|
}
|
|
}
|
|
list, err := s.ListSnapshots(user)
|
|
if err != nil {
|
|
t.Fatalf("ListSnapshots: %v", err)
|
|
}
|
|
if len(list) != 3 {
|
|
t.Fatalf("expected 3 snapshots, got %d", len(list))
|
|
}
|
|
if list[0].ID != "01ZZZ" || list[2].ID != "01ABC" {
|
|
t.Errorf("ordering wrong: %v", []string{list[0].ID, list[1].ID, list[2].ID})
|
|
}
|
|
}
|
|
|
|
func TestStorage_DeleteSnapshot_RefusesLatest(t *testing.T) {
|
|
s := newStorage(t)
|
|
sha, _ := s.WriteBlob("u", []byte("y"))
|
|
m := buildManifest("01OLD0", sha)
|
|
if err := s.StoreSnapshot("u", m, []byte("t1")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m2 := buildManifest("01NEW0", sha)
|
|
if err := s.StoreSnapshot("u", m2, []byte("t2")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Latest = 01NEW0; deleting it should fail
|
|
if err := s.DeleteSnapshot("u", "01NEW0"); err == nil {
|
|
t.Error("expected error deleting latest, got nil")
|
|
}
|
|
// Old one deletes
|
|
if err := s.DeleteSnapshot("u", "01OLD0"); err != nil {
|
|
t.Errorf("delete old: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStorage_StoreSnapshot_RejectsBadManifest(t *testing.T) {
|
|
s := newStorage(t)
|
|
bad := &Manifest{}
|
|
if err := s.StoreSnapshot("u", bad, []byte("x")); err == nil {
|
|
t.Error("expected validation error for empty manifest")
|
|
}
|
|
}
|
|
|
|
func TestStorage_RejectsBadUserID(t *testing.T) {
|
|
s := newStorage(t)
|
|
cases := []string{"", "../etc", "u/path", "user\\back", "with.dot"}
|
|
for _, u := range cases {
|
|
t.Run(u, func(t *testing.T) {
|
|
_, err := s.WriteBlob(u, []byte("x"))
|
|
if err == nil {
|
|
t.Errorf("user %q: expected error", u)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStorage_RejectsBadSnapshotID(t *testing.T) {
|
|
s := newStorage(t)
|
|
cases := []string{"", "../escape", "with/slash", strings.Repeat("a", 100), "with space"}
|
|
for _, id := range cases {
|
|
t.Run(id, func(t *testing.T) {
|
|
_, err := s.OpenSnapshot("u", id)
|
|
if err == nil {
|
|
t.Errorf("id %q: expected error", id)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStorage_UsageBytes(t *testing.T) {
|
|
s := newStorage(t)
|
|
user := "u"
|
|
_, _ = s.WriteBlob(user, []byte("0123456789")) // 10 bytes
|
|
m := buildManifest("01X", HashBytes([]byte("0123456789")))
|
|
if err := s.StoreSnapshot(user, m, make([]byte, 50)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := s.UsageBytes(user)
|
|
if err != nil {
|
|
t.Fatalf("UsageBytes: %v", err)
|
|
}
|
|
// 10 (blob) + 50 (tar) + manifest JSON on disk (variable but small)
|
|
if got < 60 {
|
|
t.Errorf("expected at least 60 bytes, got %d", got)
|
|
}
|
|
}
|