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.
110 lines
3.0 KiB
Go
110 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func goodFileEntry() FileEntry {
|
|
return FileEntry{
|
|
SHA256: strings.Repeat("ab", 32),
|
|
Size: 100,
|
|
Mtime: time.Date(2026, 6, 2, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
}
|
|
|
|
func TestManifest_Validate_OK(t *testing.T) {
|
|
m := Manifest{
|
|
SnapshotID: "01J9XQK4Z3ABCDEF",
|
|
CreatedAt: time.Now().UTC(),
|
|
Files: map[string]FileEntry{
|
|
"options.txt": goodFileEntry(),
|
|
"config/voicechat-client.json": goodFileEntry(),
|
|
"journeymap/data/sp/world/m.bin": goodFileEntry(),
|
|
},
|
|
}
|
|
if err := m.Validate(); err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestManifest_Validate_RejectsBadPaths(t *testing.T) {
|
|
cases := map[string]string{
|
|
"": "empty",
|
|
"/etc/passwd": "absolute",
|
|
"../escape": "traversal",
|
|
"./current": "non-canonical",
|
|
"config//double": "non-canonical empty segment",
|
|
`config\back`: "backslash",
|
|
"a/../../b": "traversal in middle",
|
|
}
|
|
for badPath, why := range cases {
|
|
t.Run(why, func(t *testing.T) {
|
|
m := Manifest{
|
|
SnapshotID: "x",
|
|
CreatedAt: time.Now().UTC(),
|
|
Files: map[string]FileEntry{badPath: goodFileEntry()},
|
|
}
|
|
if err := m.Validate(); err == nil {
|
|
t.Errorf("path %q (%s): expected error, got nil", badPath, why)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManifest_Validate_RejectsBadSHA(t *testing.T) {
|
|
cases := map[string]FileEntry{
|
|
"too short": {SHA256: "abcd", Size: 1, Mtime: time.Now()},
|
|
"non-hex": {SHA256: strings.Repeat("zz", 32), Size: 1, Mtime: time.Now()},
|
|
"empty": {SHA256: "", Size: 1, Mtime: time.Now()},
|
|
"negative size": {SHA256: strings.Repeat("ab", 32), Size: -1, Mtime: time.Now()},
|
|
"zero mtime": {SHA256: strings.Repeat("ab", 32), Size: 1},
|
|
}
|
|
for name, fe := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
m := Manifest{
|
|
SnapshotID: "x",
|
|
CreatedAt: time.Now().UTC(),
|
|
Files: map[string]FileEntry{"options.txt": fe},
|
|
}
|
|
if err := m.Validate(); err == nil {
|
|
t.Errorf("%s: expected error, got nil", name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManifest_Validate_EmptySnapshotID(t *testing.T) {
|
|
m := Manifest{CreatedAt: time.Now().UTC()}
|
|
if err := m.Validate(); err == nil {
|
|
t.Errorf("empty snapshot_id: expected error")
|
|
}
|
|
}
|
|
|
|
func TestReadManifest_RejectsUnknownFields(t *testing.T) {
|
|
body := `{"snapshot_id":"x","created_at":"2026-01-01T00:00:00Z","files":{},"surprise":true}`
|
|
_, err := ReadManifest(bytes.NewBufferString(body))
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown field, got nil")
|
|
}
|
|
}
|
|
|
|
func TestReadManifest_RejectsInvalidManifest(t *testing.T) {
|
|
// Well-formed JSON but bad sha
|
|
body := `{"snapshot_id":"x","created_at":"2026-01-01T00:00:00Z","files":{"options.txt":{"sha256":"short","size":1,"mtime":"2026-01-01T00:00:00Z"}}}`
|
|
_, err := ReadManifest(bytes.NewBufferString(body))
|
|
if err == nil {
|
|
t.Fatal("expected validation error")
|
|
}
|
|
}
|
|
|
|
func TestHashBytes(t *testing.T) {
|
|
got := HashBytes([]byte("hello"))
|
|
want := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
|
if got != want {
|
|
t.Errorf("got %s, want %s", got, want)
|
|
}
|
|
}
|