package loader import ( "context" "errors" "path/filepath" "testing" "galaxy/client/updater" "galaxy/connector" mc "galaxy/model/client" "galaxy/model/report" "galaxy/storage" "galaxy/storage/fs" "github.com/stretchr/testify/require" ) type stubConnector struct { versions []connector.VersionInfo versionErr error downloads map[string][]byte downloadErr error } func (c *stubConnector) CheckConnection() bool { return true } func (c *stubConnector) CheckVersion() ([]connector.VersionInfo, error) { if c.versionErr != nil { return nil, c.versionErr } return c.versions, nil } func (c *stubConnector) DownloadVersion(url string) ([]byte, error) { if c.downloadErr != nil { return nil, c.downloadErr } data, ok := c.downloads[url] if !ok { return nil, errors.New("missing download payload") } return data, nil } func (c *stubConnector) FetchReport(mc.GameID, uint, func(report.Report, error)) {} type stubRunner struct { paths []string exitCode int err error } func (r *stubRunner) Run(_ context.Context, path string) (int, error) { r.paths = append(r.paths, path) return r.exitCode, r.err } func TestRunOnceFirstLaunchDownloadsAndPromotesVersion(t *testing.T) { t.Parallel() s := newTestStorage(t) payload := []byte("ui-binary-1.2.3") info := connector.VersionInfo{ OS: "windows", Arch: "amd64", Kind: connector.ArtifactKindExecutable, Version: "1.2.3", URL: "https://example.com/ui-1.2.3.exe", Checksum: connector.NewSHA256Digest(payload), } conn := &stubConnector{ versions: []connector.VersionInfo{info}, downloads: map[string][]byte{info.URL: payload}, } runner := &stubRunner{} l := &loader{ storage: s, connector: conn, updater: updater.NewManager(s, conn, updater.WithPlatform("windows", "amd64")), runner: runner, } err := l.runOnce(context.Background()) require.NoError(t, err) state, err := s.LoadState() require.NoError(t, err) require.Equal(t, "1.2.3", state.ClientCurrentVersion) require.Nil(t, state.ClientNextVersion) expectedPath := filepath.Join(s.StorageRoot(), updater.ArtifactPath("1.2.3", "windows", "amd64", connector.ArtifactKindExecutable)) require.Equal(t, []string{expectedPath}, runner.paths) } func TestRunOnceSpawnFailureClearsPendingAndKeepsCurrent(t *testing.T) { t.Parallel() s := newTestStorage(t) currentPath := updater.ArtifactPath("1.0.0", "windows", "amd64", connector.ArtifactKindExecutable) require.NoError(t, s.WriteFile(currentPath, []byte("current"))) require.NoError(t, s.SaveState(mc.State{ClientCurrentVersion: "1.0.0"})) payload := []byte("ui-binary-1.1.0") info := connector.VersionInfo{ OS: "windows", Arch: "amd64", Kind: connector.ArtifactKindExecutable, Version: "1.1.0", URL: "https://example.com/ui-1.1.0.exe", Checksum: connector.NewSHA256Digest(payload), } conn := &stubConnector{ versions: []connector.VersionInfo{info}, downloads: map[string][]byte{info.URL: payload}, } manager := updater.NewManager(s, conn, updater.WithPlatform("windows", "amd64")) require.NoError(t, manager.CheckAndPrepareLatest()) l := &loader{ storage: s, connector: conn, updater: manager, runner: &stubRunner{ err: errors.New("spawn failed"), }, } err := l.runOnce(context.Background()) require.Error(t, err) state, err := s.LoadState() require.NoError(t, err) require.Equal(t, "1.0.0", state.ClientCurrentVersion) require.Nil(t, state.ClientNextVersion) currentExists, _, err := s.FileExists(currentPath) require.NoError(t, err) require.True(t, currentExists) nextExists, _, err := s.FileExists(updater.ArtifactPath("1.1.0", "windows", "amd64", connector.ArtifactKindExecutable)) require.NoError(t, err) require.False(t, nextExists) } func TestRunOnceNonZeroExitClearsPendingAndKeepsCurrent(t *testing.T) { t.Parallel() s := newTestStorage(t) currentPath := updater.ArtifactPath("1.0.0", "windows", "amd64", connector.ArtifactKindExecutable) require.NoError(t, s.WriteFile(currentPath, []byte("current"))) require.NoError(t, s.SaveState(mc.State{ClientCurrentVersion: "1.0.0"})) payload := []byte("ui-binary-1.1.0") info := connector.VersionInfo{ OS: "windows", Arch: "amd64", Kind: connector.ArtifactKindExecutable, Version: "1.1.0", URL: "https://example.com/ui-1.1.0.exe", Checksum: connector.NewSHA256Digest(payload), } conn := &stubConnector{ versions: []connector.VersionInfo{info}, downloads: map[string][]byte{info.URL: payload}, } manager := updater.NewManager(s, conn, updater.WithPlatform("windows", "amd64")) require.NoError(t, manager.CheckAndPrepareLatest()) l := &loader{ storage: s, connector: conn, updater: manager, runner: &stubRunner{ exitCode: 23, }, } err := l.runOnce(context.Background()) require.Error(t, err) state, err := s.LoadState() require.NoError(t, err) require.Equal(t, "1.0.0", state.ClientCurrentVersion) require.Nil(t, state.ClientNextVersion) nextExists, _, err := s.FileExists(updater.ArtifactPath("1.1.0", "windows", "amd64", connector.ArtifactKindExecutable)) require.NoError(t, err) require.False(t, nextExists) } func newTestStorage(t *testing.T) *testStorage { t.Helper() root := t.TempDir() s, err := fs.NewFS(root) require.NoError(t, err) return &testStorage{Storage: s, root: root} } type testStorage struct { storage.Storage root string } func (s *testStorage) StorageRoot() string { return s.root }