Files
cloud-svc/storage_test.go
T
claude-timemachine 1752ef05a6
CI / validate (push) Successful in 26s
CI / docker (push) Failing after 8s
initial: Steam-Cloud-style per-user state sync skeleton
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.
2026-06-02 18:52:25 +02:00

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)
}
}