246 lines
8.9 KiB
Go
246 lines
8.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
|
)
|
|
|
|
func TestDiscoverRemoteCurrentStateSuccess(t *testing.T) {
|
|
cfg := restoreDiscoveryConfig()
|
|
store := &storage.FakeBackend{}
|
|
sessionPrefix, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
|
|
|
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
|
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)})
|
|
|
|
state, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
|
if err != nil {
|
|
t.Fatalf("discoverRemoteCurrentState() error = %v", err)
|
|
}
|
|
if state.RunID != "20260519T010203Z-a1b2c3d4" {
|
|
t.Fatalf("run id = %q, want 20260519T010203Z-a1b2c3d4", state.RunID)
|
|
}
|
|
if state.SessionPrefix != sessionPrefix {
|
|
t.Fatalf("session prefix = %q, want %q", state.SessionPrefix, sessionPrefix)
|
|
}
|
|
if state.CurrentRunIDKey != runIDKey {
|
|
t.Fatalf("current run id key = %q, want %q", state.CurrentRunIDKey, runIDKey)
|
|
}
|
|
if state.CurrentManifestKey != manifestKey {
|
|
t.Fatalf("current manifest key = %q, want %q", state.CurrentManifestKey, manifestKey)
|
|
}
|
|
if state.Manifest == nil {
|
|
t.Fatal("manifest is nil")
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRemoteCurrentStateMissingRunPointerFails(t *testing.T) {
|
|
cfg := restoreDiscoveryConfig()
|
|
store := &storage.FakeBackend{}
|
|
|
|
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
|
if err == nil || !strings.Contains(err.Error(), "remote current run pointer missing") {
|
|
t.Fatalf("error = %v, want missing run pointer failure", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRemoteCurrentStateEmptyRunPointerFails(t *testing.T) {
|
|
cfg := restoreDiscoveryConfig()
|
|
store := &storage.FakeBackend{}
|
|
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
|
|
|
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte(" \n\t")})
|
|
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)})
|
|
|
|
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
|
if err == nil || !strings.Contains(err.Error(), "is empty") {
|
|
t.Fatalf("error = %v, want empty run pointer failure", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRemoteCurrentStateMissingManifestFails(t *testing.T) {
|
|
cfg := restoreDiscoveryConfig()
|
|
store := &storage.FakeBackend{}
|
|
_, _, runIDKey := restoreDiscoveryKeys(cfg)
|
|
|
|
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
|
|
|
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
|
if err == nil || !strings.Contains(err.Error(), "remote current manifest missing") {
|
|
t.Fatalf("error = %v, want missing manifest failure", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRemoteCurrentStateInvalidManifestFails(t *testing.T) {
|
|
cfg := restoreDiscoveryConfig()
|
|
store := &storage.FakeBackend{}
|
|
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
|
|
|
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
|
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: []byte("{invalid json")})
|
|
|
|
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
|
if err == nil || !strings.Contains(err.Error(), "remote current manifest decode failed") {
|
|
t.Fatalf("error = %v, want manifest decode failure", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRemoteCurrentStateSessionMismatchFails(t *testing.T) {
|
|
cfg := restoreDiscoveryConfig()
|
|
store := &storage.FakeBackend{}
|
|
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
|
|
|
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
|
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "wrong-session", cfg.Session.Campaign)})
|
|
|
|
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
|
if err == nil || !strings.Contains(err.Error(), "does not match expected session_id") {
|
|
t.Fatalf("error = %v, want session mismatch failure", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRemoteCurrentStateCampaignMismatchFails(t *testing.T) {
|
|
cfg := restoreDiscoveryConfig()
|
|
store := &storage.FakeBackend{}
|
|
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
|
|
|
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
|
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, "wrong-campaign")})
|
|
|
|
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
|
if err == nil || !strings.Contains(err.Error(), "does not match expected campaign") {
|
|
t.Fatalf("error = %v, want campaign mismatch failure", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRemoteCurrentStateEmptyCampaignFails(t *testing.T) {
|
|
cfg := restoreDiscoveryConfig()
|
|
store := &storage.FakeBackend{}
|
|
_, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
|
|
|
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
|
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, "")})
|
|
|
|
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
|
if err == nil || !strings.Contains(err.Error(), "campaign is required") {
|
|
t.Fatalf("error = %v, want empty campaign failure", err)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRemoteCurrentStateUsesCurrentKeysUnderSessionPrefix(t *testing.T) {
|
|
cfg := restoreDiscoveryConfig()
|
|
sessionPrefix, manifestKey, runIDKey := restoreDiscoveryKeys(cfg)
|
|
base := &storage.FakeBackend{}
|
|
store := &captureObjectStore{delegate: base}
|
|
|
|
base.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
|
base.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)})
|
|
|
|
_, err := discoverRemoteCurrentState(context.Background(), cfg, store)
|
|
if err != nil {
|
|
t.Fatalf("discoverRemoteCurrentState() error = %v", err)
|
|
}
|
|
|
|
expectedRunKey := fmt.Sprintf("%scurrent/run_id.txt", sessionPrefix)
|
|
expectedManifestKey := fmt.Sprintf("%scurrent/manifest.json", sessionPrefix)
|
|
if !containsString(store.readKeys, expectedRunKey) {
|
|
t.Fatalf("read keys = %#v, want run pointer key %q", store.readKeys, expectedRunKey)
|
|
}
|
|
if !containsString(store.readKeys, expectedManifestKey) {
|
|
t.Fatalf("read keys = %#v, want manifest key %q", store.readKeys, expectedManifestKey)
|
|
}
|
|
}
|
|
|
|
type captureObjectStore struct {
|
|
delegate storage.ObjectStore
|
|
existsKeys []string
|
|
readKeys []string
|
|
downloadKeys []string
|
|
}
|
|
|
|
func (s *captureObjectStore) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) {
|
|
return s.delegate.List(ctx, prefix)
|
|
}
|
|
|
|
func (s *captureObjectStore) Read(ctx context.Context, key string) (storage.ObjectInfo, io.ReadCloser, error) {
|
|
s.readKeys = append(s.readKeys, key)
|
|
return s.delegate.Read(ctx, key)
|
|
}
|
|
|
|
func (s *captureObjectStore) Download(ctx context.Context, key, localPath string) error {
|
|
s.downloadKeys = append(s.downloadKeys, key)
|
|
return s.delegate.Download(ctx, key, localPath)
|
|
}
|
|
|
|
func (s *captureObjectStore) Upload(ctx context.Context, localPath, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) {
|
|
return s.delegate.Upload(ctx, localPath, key, opts)
|
|
}
|
|
|
|
func (s *captureObjectStore) UploadConditional(ctx context.Context, source io.Reader, key string, opts storage.UploadOptions, condition storage.WriteCondition) (storage.ObjectInfo, error) {
|
|
return s.delegate.UploadConditional(ctx, source, key, opts, condition)
|
|
}
|
|
|
|
func (s *captureObjectStore) Exists(ctx context.Context, key string) (bool, error) {
|
|
s.existsKeys = append(s.existsKeys, key)
|
|
return s.delegate.Exists(ctx, key)
|
|
}
|
|
|
|
func restoreDiscoveryConfig() *config.Config {
|
|
return &config.Config{
|
|
Pipeline: &config.PipelineConfig{
|
|
Storage: config.StorageConfig{
|
|
S3: &config.StorageS3Config{
|
|
Bucket: "my-dnd-archive",
|
|
RootPrefix: "dnd",
|
|
},
|
|
},
|
|
},
|
|
Session: &config.SessionConfig{
|
|
SessionID: "2026-05-03",
|
|
Campaign: "sample-campaign",
|
|
},
|
|
}
|
|
}
|
|
|
|
func restoreDiscoveryKeys(cfg *config.Config) (sessionPrefix, manifestKey, runIDKey string) {
|
|
sessionPrefix = artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
|
|
manifestKey, runIDKey = artifacts.ResolveCurrentStateKeys(sessionPrefix)
|
|
return sessionPrefix, manifestKey, runIDKey
|
|
}
|
|
|
|
func restoreManifestJSON(t *testing.T, sessionID, campaign string) []byte {
|
|
t.Helper()
|
|
now := time.Date(2026, 5, 19, 23, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)
|
|
payload := map[string]any{
|
|
"session_id": sessionID,
|
|
"campaign": campaign,
|
|
"run_id": "20260519T010203Z-a1b2c3d4",
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
"stages": map[string]any{},
|
|
}
|
|
data, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal manifest payload: %v", err)
|
|
}
|
|
return append(data, '\n')
|
|
}
|
|
|
|
func containsString(values []string, target string) bool {
|
|
for _, value := range values {
|
|
if value == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|