555 lines
20 KiB
Go
555 lines
20 KiB
Go
package evidence
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"errors"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"cmbuyer/admin/internal/deviceauth"
|
|
core "cmbuyer/admin/internal/evidence"
|
|
"cmbuyer/admin/internal/migrations"
|
|
"cmbuyer/admin/internal/storage/sqlite"
|
|
)
|
|
|
|
const (
|
|
testTaskID = "13c9f507-7473-4fa6-8d71-8786c34c6301"
|
|
testAuthID = "23c9f507-7473-4fa6-8d71-8786c34c6301"
|
|
testAttemptID = "33c9f507-7473-4fa6-8d71-8786c34c6301"
|
|
testUploadKey = "43c9f507-7473-4fa6-8d71-8786c34c6301"
|
|
testDeviceID = "53c9f507-7473-4fa6-8d71-8786c34c6301"
|
|
)
|
|
|
|
func TestStageCommitReplayAndOpen(t *testing.T) {
|
|
database, store := newTestStore(t)
|
|
insertAttemptFixture(t, database)
|
|
pngBytes := makePNG(t, 8, 6)
|
|
hash := sha256Hex(pngBytes)
|
|
metadata := core.UploadMetadata{UploadKey: testUploadKey, TaskID: testTaskID, AttemptID: testAttemptID, Kind: core.KindSKUPanelGate1, PrivacyTier: core.PrivacyInternalRaw, SHA256: hash, CapturedAt: time.Date(2026, 8, 4, 1, 2, 3, 0, time.UTC)}
|
|
principal := deviceauth.Principal{ID: testDeviceID}
|
|
|
|
staged, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("Stage: %v", err)
|
|
}
|
|
asset, replayed, err := store.Commit(context.Background(), principal, metadata, staged)
|
|
if err != nil || replayed {
|
|
t.Fatalf("Commit = replayed %t, err %v", replayed, err)
|
|
}
|
|
if asset.SHA256 != hash || asset.ByteSize != int64(len(pngBytes)) || asset.Width != 8 || asset.Height != 6 || asset.StorageKey != hash[:2]+"/"+hash+".png" {
|
|
t.Fatalf("asset = %#v", asset)
|
|
}
|
|
opened, reader, err := store.Open(context.Background(), asset.ID)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
got, err := io.ReadAll(reader)
|
|
_ = reader.Close()
|
|
if err != nil || !bytes.Equal(got, pngBytes) || opened.ID != asset.ID {
|
|
t.Fatalf("opened asset changed: bytes=%t asset=%#v err=%v", bytes.Equal(got, pngBytes), opened, err)
|
|
}
|
|
|
|
replayStage, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("stage replay: %v", err)
|
|
}
|
|
replayedAsset, replayed, err := store.Commit(context.Background(), principal, metadata, replayStage)
|
|
if err != nil || !replayed || replayedAsset.ID != asset.ID {
|
|
t.Fatalf("replay = %#v, %t, %v", replayedAsset, replayed, err)
|
|
}
|
|
|
|
conflictStage, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("stage conflict: %v", err)
|
|
}
|
|
conflicting := metadata
|
|
conflicting.CapturedAt = conflicting.CapturedAt.Add(time.Second)
|
|
if _, _, err := store.Commit(context.Background(), principal, conflicting, conflictStage); !errors.Is(err, core.ErrConflict) {
|
|
t.Fatalf("conflicting replay error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestConcurrentReplayCreatesOneAsset(t *testing.T) {
|
|
database, store := newTestStore(t)
|
|
insertAttemptFixture(t, database)
|
|
pngBytes := makePNG(t, 3, 2)
|
|
metadata := core.UploadMetadata{UploadKey: testUploadKey, TaskID: testTaskID, AttemptID: testAttemptID, Kind: core.KindSKUPanelGate1, PrivacyTier: core.PrivacyInternalRaw, SHA256: sha256Hex(pngBytes), CapturedAt: time.Date(2026, 8, 4, 1, 2, 3, 0, time.UTC)}
|
|
staged := make([]core.StagedFile, 2)
|
|
for index := range staged {
|
|
var err error
|
|
staged[index], err = store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("Stage %d: %v", index, err)
|
|
}
|
|
}
|
|
var wait sync.WaitGroup
|
|
wait.Add(2)
|
|
assets := make([]core.Asset, 2)
|
|
replays := make([]bool, 2)
|
|
errorsSeen := make([]error, 2)
|
|
for index := range staged {
|
|
go func(index int) {
|
|
defer wait.Done()
|
|
assets[index], replays[index], errorsSeen[index] = store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, metadata, staged[index])
|
|
}(index)
|
|
}
|
|
wait.Wait()
|
|
if errorsSeen[0] != nil || errorsSeen[1] != nil || assets[0].ID != assets[1].ID || replays[0] == replays[1] {
|
|
t.Fatalf("concurrent commits assets=%#v replays=%#v errors=%#v", assets, replays, errorsSeen)
|
|
}
|
|
var count int
|
|
if err := database.QueryRow("SELECT COUNT(*) FROM evidence_assets").Scan(&count); err != nil || count != 1 {
|
|
t.Fatalf("asset count = %d, err %v", count, err)
|
|
}
|
|
}
|
|
|
|
func TestPlatformDirectorySync(t *testing.T) {
|
|
if err := syncDirectory(t.TempDir()); err != nil {
|
|
t.Fatalf("syncDirectory must either establish the durability boundary or fail closed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewStoreRetriesRootParentSyncWhenRootAlreadyExists(t *testing.T) {
|
|
database, _ := newTestStore(t)
|
|
parent := t.TempDir()
|
|
root := filepath.Join(parent, "retry-root")
|
|
injected := errors.New("injected root parent sync failure")
|
|
if _, err := newStore(database, root, func(string) error { return injected }); !errors.Is(err, injected) {
|
|
t.Fatalf("first newStore error = %v, want injected root sync failure", err)
|
|
}
|
|
if info, err := os.Stat(root); err != nil || !info.IsDir() {
|
|
t.Fatalf("failed parent sync must leave root for retry: info=%v err=%v", info, err)
|
|
}
|
|
|
|
var paths []string
|
|
store, err := newStore(database, root, func(path string) error {
|
|
paths = append(paths, path)
|
|
return syncDirectory(path)
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("retry newStore: %v", err)
|
|
}
|
|
if store == nil || len(paths) == 0 || paths[0] != parent {
|
|
t.Fatalf("retry sync paths = %#v, want root parent %q first", paths, parent)
|
|
}
|
|
}
|
|
|
|
func TestCommitSyncsShardAndRenameBeforeDatabaseWrite(t *testing.T) {
|
|
database, store := newTestStore(t)
|
|
insertAttemptFixture(t, database)
|
|
pngBytes := makePNG(t, 4, 3)
|
|
hash := sha256Hex(pngBytes)
|
|
metadata := testMetadata(hash)
|
|
staged, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("Stage: %v", err)
|
|
}
|
|
finalPath, err := store.pathForKey(storageKey(hash))
|
|
if err != nil {
|
|
t.Fatalf("final path: %v", err)
|
|
}
|
|
finalDirectory := filepath.Dir(finalPath)
|
|
var events []string
|
|
store.syncDirectory = func(path string) error {
|
|
var count int
|
|
if err := database.QueryRow("SELECT COUNT(*) FROM evidence_assets").Scan(&count); err != nil {
|
|
t.Fatalf("count evidence before directory sync: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("database row became visible before directory sync: %d", count)
|
|
}
|
|
switch path {
|
|
case store.root:
|
|
events = append(events, "sync-root")
|
|
if path != store.root {
|
|
t.Fatalf("shard parent sync path = %q, want evidence root %q", path, store.root)
|
|
}
|
|
if info, err := os.Stat(finalDirectory); err != nil || !info.IsDir() {
|
|
t.Fatalf("shard directory must exist before parent sync: info=%v err=%v", info, err)
|
|
}
|
|
if _, err := os.Stat(finalPath); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("final file exists before publication: %v", err)
|
|
}
|
|
case finalDirectory:
|
|
events = append(events, "sync-shard")
|
|
if info, err := os.Stat(finalPath); err != nil || !info.Mode().IsRegular() {
|
|
t.Fatalf("renamed file must exist before shard sync: info=%v err=%v", info, err)
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected extra directory sync: %q", path)
|
|
}
|
|
return syncDirectory(path)
|
|
}
|
|
store.syncFile = func(file *os.File) error {
|
|
if filepath.Dir(file.Name()) != finalDirectory || !strings.HasPrefix(filepath.Base(file.Name()), ".publish-") {
|
|
t.Fatalf("publication temp is not inside shard: %q", file.Name())
|
|
}
|
|
events = append(events, "sync-file")
|
|
return file.Sync()
|
|
}
|
|
store.renameFile = func(oldPath, newPath string) error {
|
|
if filepath.Dir(oldPath) != filepath.Dir(newPath) || newPath != finalPath {
|
|
t.Fatalf("rename is not same-directory publication: %q -> %q", oldPath, newPath)
|
|
}
|
|
events = append(events, "rename")
|
|
return os.Rename(oldPath, newPath)
|
|
}
|
|
|
|
if _, replayed, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, metadata, staged); err != nil || replayed {
|
|
t.Fatalf("Commit = replayed %t, err %v", replayed, err)
|
|
}
|
|
if got, want := strings.Join(events, ","), "sync-root,sync-root,sync-file,rename,sync-shard"; got != want {
|
|
t.Fatalf("durability order = %q, want %q", got, want)
|
|
}
|
|
assertEvidenceCount(t, database, 1)
|
|
assertNoPublishTemps(t, finalDirectory)
|
|
}
|
|
|
|
func TestCommitDirectorySyncFailuresNeverWriteDatabase(t *testing.T) {
|
|
for _, failAt := range []int{1, 2, 3} {
|
|
t.Run(map[int]string{1: "new shard parent", 2: "unconditional shard parent", 3: "rename target"}[failAt], func(t *testing.T) {
|
|
database, store := newTestStore(t)
|
|
insertAttemptFixture(t, database)
|
|
pngBytes := makePNG(t, 4, 3)
|
|
hash := sha256Hex(pngBytes)
|
|
staged, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("Stage: %v", err)
|
|
}
|
|
finalPath, err := store.pathForKey(storageKey(hash))
|
|
if err != nil {
|
|
t.Fatalf("final path: %v", err)
|
|
}
|
|
injected := errors.New("injected directory sync failure")
|
|
calls := 0
|
|
store.syncDirectory = func(path string) error {
|
|
calls++
|
|
if calls == failAt {
|
|
return injected
|
|
}
|
|
return syncDirectory(path)
|
|
}
|
|
|
|
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), staged); !errors.Is(err, injected) {
|
|
t.Fatalf("Commit error = %v, want injected sync failure", err)
|
|
}
|
|
if calls != failAt {
|
|
t.Fatalf("sync calls = %d, want %d", calls, failAt)
|
|
}
|
|
assertEvidenceCount(t, database, 0)
|
|
_, statErr := os.Stat(finalPath)
|
|
if failAt < 3 && !errors.Is(statErr, os.ErrNotExist) {
|
|
t.Fatalf("file exists before rename durability boundary: %v", statErr)
|
|
}
|
|
if failAt == 3 && statErr != nil {
|
|
t.Fatalf("post-rename sync failure may leave an orphan file, stat error = %v", statErr)
|
|
}
|
|
assertNoPublishTemps(t, filepath.Dir(finalPath))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommitRetriesShardParentSyncAfterPriorFailure(t *testing.T) {
|
|
database, store := newTestStore(t)
|
|
insertAttemptFixture(t, database)
|
|
pngBytes := makePNG(t, 4, 3)
|
|
hash := sha256Hex(pngBytes)
|
|
finalPath, err := store.pathForKey(storageKey(hash))
|
|
if err != nil {
|
|
t.Fatalf("final path: %v", err)
|
|
}
|
|
firstStage, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("first Stage: %v", err)
|
|
}
|
|
injected := errors.New("injected first shard parent sync failure")
|
|
store.syncDirectory = func(string) error { return injected }
|
|
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), firstStage); !errors.Is(err, injected) {
|
|
t.Fatalf("first Commit error = %v", err)
|
|
}
|
|
if info, err := os.Stat(filepath.Dir(finalPath)); err != nil || !info.IsDir() {
|
|
t.Fatalf("failed first sync must leave the created shard for retry: info=%v err=%v", info, err)
|
|
}
|
|
assertEvidenceCount(t, database, 0)
|
|
|
|
secondStage, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("second Stage: %v", err)
|
|
}
|
|
var paths []string
|
|
store.syncDirectory = func(path string) error {
|
|
paths = append(paths, path)
|
|
return syncDirectory(path)
|
|
}
|
|
if _, replayed, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), secondStage); err != nil || replayed {
|
|
t.Fatalf("retry Commit = replayed %t, err %v", replayed, err)
|
|
}
|
|
if len(paths) != 2 || paths[0] != store.root || paths[1] != filepath.Dir(finalPath) {
|
|
t.Fatalf("retry sync paths = %#v, want root then shard", paths)
|
|
}
|
|
assertEvidenceCount(t, database, 1)
|
|
assertNoPublishTemps(t, filepath.Dir(finalPath))
|
|
}
|
|
|
|
func TestCommitPublicationFailuresCleanTempAndNeverWriteDatabase(t *testing.T) {
|
|
for _, name := range []string{"file sync", "rename"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
database, store := newTestStore(t)
|
|
insertAttemptFixture(t, database)
|
|
pngBytes := makePNG(t, 4, 3)
|
|
hash := sha256Hex(pngBytes)
|
|
staged, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("Stage: %v", err)
|
|
}
|
|
finalPath, err := store.pathForKey(storageKey(hash))
|
|
if err != nil {
|
|
t.Fatalf("final path: %v", err)
|
|
}
|
|
injected := errors.New("injected publication failure")
|
|
if name == "file sync" {
|
|
store.syncFile = func(*os.File) error { return injected }
|
|
} else {
|
|
store.renameFile = func(string, string) error { return injected }
|
|
}
|
|
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), staged); !errors.Is(err, injected) {
|
|
t.Fatalf("Commit error = %v, want injected publication failure", err)
|
|
}
|
|
if _, err := os.Stat(finalPath); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("final file exists after failed publication: %v", err)
|
|
}
|
|
assertNoPublishTemps(t, filepath.Dir(finalPath))
|
|
assertEvidenceCount(t, database, 0)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommitDatabaseFailuresAfterDurableRenameLeaveOnlyOrphan(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
inject func(*testing.T, *sql.DB, *Store, error)
|
|
}{
|
|
{
|
|
name: "insert",
|
|
inject: func(t *testing.T, database *sql.DB, _ *Store, _ error) {
|
|
t.Helper()
|
|
if _, err := database.Exec(`CREATE TRIGGER fail_evidence_insert BEFORE INSERT ON evidence_assets BEGIN SELECT RAISE(ABORT, 'injected insert failure'); END`); err != nil {
|
|
t.Fatalf("create insert failure trigger: %v", err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "commit",
|
|
inject: func(_ *testing.T, _ *sql.DB, store *Store, injected error) {
|
|
store.commitTx = func(*sql.Tx) error { return injected }
|
|
},
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
database, store := newTestStore(t)
|
|
insertAttemptFixture(t, database)
|
|
pngBytes := makePNG(t, 4, 3)
|
|
hash := sha256Hex(pngBytes)
|
|
staged, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("Stage: %v", err)
|
|
}
|
|
finalPath, err := store.pathForKey(storageKey(hash))
|
|
if err != nil {
|
|
t.Fatalf("final path: %v", err)
|
|
}
|
|
injected := errors.New("injected database failure")
|
|
test.inject(t, database, store, injected)
|
|
syncCalls := 0
|
|
store.syncDirectory = func(path string) error {
|
|
syncCalls++
|
|
return syncDirectory(path)
|
|
}
|
|
|
|
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, testMetadata(hash), staged); err == nil {
|
|
t.Fatal("Commit unexpectedly succeeded")
|
|
}
|
|
if syncCalls != 3 {
|
|
t.Fatalf("database failure occurred before both durability syncs: sync calls = %d", syncCalls)
|
|
}
|
|
if info, err := os.Stat(finalPath); err != nil || !info.Mode().IsRegular() {
|
|
t.Fatalf("durable rename may leave only an orphan file: info=%v err=%v", info, err)
|
|
}
|
|
assertNoPublishTemps(t, filepath.Dir(finalPath))
|
|
assertEvidenceCount(t, database, 0)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStageRejectsUnsafeContent(t *testing.T) {
|
|
_, store := newTestStore(t)
|
|
largePNG := makePNG(t, core.MaxImageSide+1, 1)
|
|
pngWithXML := append(makePNG(t, 1, 1), []byte("<hierarchy/>")...)
|
|
for name, test := range map[string]struct {
|
|
reader io.Reader
|
|
contentType string
|
|
}{
|
|
"wrong content type": {reader: bytes.NewReader(makePNG(t, 1, 1)), contentType: "application/octet-stream"},
|
|
"xml": {reader: bytes.NewBufferString("<hierarchy/>"), contentType: core.PNGContentType},
|
|
"png with xml tail": {reader: bytes.NewReader(pngWithXML), contentType: core.PNGContentType},
|
|
"truncated png": {reader: bytes.NewReader(pngSignature), contentType: core.PNGContentType},
|
|
"too wide": {reader: bytes.NewReader(largePNG), contentType: core.PNGContentType},
|
|
"too many bytes": {reader: io.LimitReader(zeroReader{}, core.MaxFileBytes+1), contentType: core.PNGContentType},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
staged, err := store.Stage(test.reader, test.contentType)
|
|
if !errors.Is(err, core.ErrInvalid) && !errors.Is(err, core.ErrTooLarge) {
|
|
store.Discard(staged)
|
|
t.Fatalf("Stage error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommitRequiresAttemptOwnedByTaskAndLowercaseHash(t *testing.T) {
|
|
database, store := newTestStore(t)
|
|
insertAttemptFixture(t, database)
|
|
pngBytes := makePNG(t, 2, 2)
|
|
base := core.UploadMetadata{UploadKey: testUploadKey, TaskID: testTaskID, AttemptID: testAttemptID, Kind: core.KindSKUPanelGate1, PrivacyTier: core.PrivacyInternalRaw, SHA256: sha256Hex(pngBytes), CapturedAt: time.Date(2026, 8, 4, 1, 2, 3, 0, time.UTC)}
|
|
for name, mutate := range map[string]func(*core.UploadMetadata){
|
|
"unknown attempt": func(value *core.UploadMetadata) { value.AttemptID = "53c9f507-7473-4fa6-8d71-8786c34c6301" },
|
|
"uppercase hash": func(value *core.UploadMetadata) {
|
|
value.SHA256 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
},
|
|
"wrong kind": func(value *core.UploadMetadata) { value.Kind = "ORDER_CONFIRM" },
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
staged, err := store.Stage(bytes.NewReader(pngBytes), core.PNGContentType)
|
|
if err != nil {
|
|
t.Fatalf("Stage: %v", err)
|
|
}
|
|
metadata := base
|
|
mutate(&metadata)
|
|
if _, _, err := store.Commit(context.Background(), deviceauth.Principal{ID: testDeviceID}, metadata, staged); !errors.Is(err, core.ErrInvalid) {
|
|
t.Fatalf("Commit error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
var count int
|
|
if err := database.QueryRow("SELECT COUNT(*) FROM evidence_assets").Scan(&count); err != nil || count != 0 {
|
|
t.Fatalf("invalid commits created %d assets, err %v", count, err)
|
|
}
|
|
}
|
|
|
|
func TestNewStoreRejectsRelativeAndFilesystemRootPaths(t *testing.T) {
|
|
database, _ := newTestStore(t)
|
|
if _, err := NewStore(database, "relative-evidence"); err == nil {
|
|
t.Fatal("relative evidence root succeeded")
|
|
}
|
|
volumeRoot := filepath.VolumeName(t.TempDir()) + string(filepath.Separator)
|
|
if _, err := NewStore(database, volumeRoot); err == nil {
|
|
t.Fatal("filesystem root succeeded")
|
|
}
|
|
}
|
|
|
|
type zeroReader struct{}
|
|
|
|
func (zeroReader) Read(buffer []byte) (int, error) {
|
|
for index := range buffer {
|
|
buffer[index] = 0
|
|
}
|
|
return len(buffer), nil
|
|
}
|
|
|
|
func newTestStore(t *testing.T) (*sql.DB, *Store) {
|
|
t.Helper()
|
|
database, err := sqlite.Open(filepath.Join(t.TempDir(), "evidence.db"))
|
|
if err != nil {
|
|
t.Fatalf("open database: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = database.Close() })
|
|
_, file, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("locate test")
|
|
}
|
|
directory := filepath.Join(filepath.Dir(file), "..", "..", "..", "migrations")
|
|
if err := migrations.Up(context.Background(), database, directory); err != nil {
|
|
t.Fatalf("migrate database: %v", err)
|
|
}
|
|
store, err := NewStore(database, filepath.Join(t.TempDir(), "assets"))
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
return database, store
|
|
}
|
|
|
|
func insertAttemptFixture(t *testing.T, database *sql.DB) {
|
|
t.Helper()
|
|
timestamp := "2026-08-04T00:00:00Z"
|
|
if _, err := database.Exec(`INSERT INTO tasks (id, source, title, goods_id, sku_color, sku_size, quantity, max_total_price, status, version, created_at, updated_at) VALUES (?, 'MANUAL', 'task', '123', 'black', 'M', 1, '1.00', 'DRAFT', 1, ?, ?)`, testTaskID, timestamp, timestamp); err != nil {
|
|
t.Fatalf("insert task: %v", err)
|
|
}
|
|
if _, err := database.Exec(`INSERT INTO order_authorizations (id, task_id, task_version, start_key, goods_id, sku_color, sku_size, quantity, total_price_cap, status, created_by, created_at, expires_at) VALUES (?, ?, 1, 'start', '123', 'black', 'M', 1, '1.00', 'ACTIVE', 'admin', ?, ?)`, testAuthID, testTaskID, timestamp, timestamp); err != nil {
|
|
t.Fatalf("insert authorization: %v", err)
|
|
}
|
|
if _, err := database.Exec(`INSERT INTO purchase_attempts (id, task_id, authorization_id, claim_generation, status, started_at) VALUES (?, ?, ?, 1, 'CLAIMED', ?)`, testAttemptID, testTaskID, testAuthID, timestamp); err != nil {
|
|
t.Fatalf("insert attempt: %v", err)
|
|
}
|
|
}
|
|
|
|
func testMetadata(hash string) core.UploadMetadata {
|
|
return core.UploadMetadata{
|
|
UploadKey: testUploadKey, TaskID: testTaskID, AttemptID: testAttemptID,
|
|
Kind: core.KindSKUPanelGate1, PrivacyTier: core.PrivacyInternalRaw, SHA256: hash,
|
|
CapturedAt: time.Date(2026, 8, 4, 1, 2, 3, 0, time.UTC),
|
|
}
|
|
}
|
|
|
|
func assertEvidenceCount(t *testing.T, database *sql.DB, want int) {
|
|
t.Helper()
|
|
var count int
|
|
if err := database.QueryRow("SELECT COUNT(*) FROM evidence_assets").Scan(&count); err != nil {
|
|
t.Fatalf("count evidence assets: %v", err)
|
|
}
|
|
if count != want {
|
|
t.Fatalf("evidence asset count = %d, want %d", count, want)
|
|
}
|
|
}
|
|
|
|
func assertNoPublishTemps(t *testing.T, directory string) {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(directory)
|
|
if err != nil {
|
|
t.Fatalf("read shard directory: %v", err)
|
|
}
|
|
for _, entry := range entries {
|
|
if strings.HasPrefix(entry.Name(), ".publish-") {
|
|
t.Fatalf("publication temp leaked: %q", entry.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
func makePNG(t *testing.T, width, height int) []byte {
|
|
t.Helper()
|
|
imageData := image.NewNRGBA(image.Rect(0, 0, width, height))
|
|
imageData.Set(0, 0, color.NRGBA{R: 12, G: 34, B: 56, A: 255})
|
|
var buffer bytes.Buffer
|
|
if err := png.Encode(&buffer, imageData); err != nil {
|
|
t.Fatalf("encode PNG: %v", err)
|
|
}
|
|
return buffer.Bytes()
|
|
}
|
|
|
|
func sha256Hex(value []byte) string {
|
|
hash := sha256.Sum256(value)
|
|
return hex.EncodeToString(hash[:])
|
|
}
|