From e6c69709479904bcf7ecaca0d42e6c6b758c241f Mon Sep 17 00:00:00 2001 From: IliaDenisov Date: Mon, 16 Mar 2026 15:48:00 +0200 Subject: [PATCH] loader logic revised --- client/client.go | 12 ++- client/loader/client.go | 25 ++++++ client/loader/download.go | 81 +++++++++++++++++-- client/loader/loader.go | 135 ++++++++++++++++++-------------- client/loader/service.go | 34 ++++++++ client/loader/state.go | 61 +++++++++++++++ client/loader/util.go | 22 +++++- client/loader/util_test.go | 56 +++++++++++++ pkg/connector/connector.go | 75 +++++++++++++++++- pkg/connector/connector_test.go | 89 +++++++++++++++++++++ pkg/model/client/client.go | 13 ++- pkg/storage/fs/fs.go | 2 +- pkg/storage/fs/fs_test.go | 7 +- 13 files changed, 530 insertions(+), 82 deletions(-) create mode 100644 client/loader/client.go create mode 100644 client/loader/service.go create mode 100644 client/loader/state.go create mode 100644 client/loader/util_test.go create mode 100644 pkg/connector/connector_test.go diff --git a/client/client.go b/client/client.go index 4850aad..c912b45 100644 --- a/client/client.go +++ b/client/client.go @@ -206,10 +206,14 @@ func (e *client) Run() error { return nil } -func (e *client) Shutdown() { - e.window.Close() -} +func (e *client) Shutdown() { e.window.Close() } + +func (e *client) Version() string { return version } func (e *client) OnConnection(bool) {} -func (e *client) Version() string { return version } +func (e *client) OnConnectionError(error) {} + +func (e *client) OnStorageError(error) {} + +func (e *client) OnServiceError(error) {} diff --git a/client/loader/client.go b/client/loader/client.go new file mode 100644 index 0000000..6adf64f --- /dev/null +++ b/client/loader/client.go @@ -0,0 +1,25 @@ +package loader + +// onConnectionError обёртка над событием OnConnectionError для UI +func (l *loader) onConnectionError(err error) { + if err == nil || l.client == nil { + return + } + l.client.OnConnectionError(err) +} + +// onStorageError обёртка над событием OnStorageError для UI +func (l *loader) onStorageError(err error) { + if err == nil || l.client == nil { + return + } + l.client.OnStorageError(err) +} + +// onServiceError обёртка над событием OnServiceError для UI +func (l *loader) onServiceError(err error) { + if err == nil || l.client == nil { + return + } + l.client.OnServiceError(err) +} diff --git a/client/loader/download.go b/client/loader/download.go index 4541446..e36fe49 100644 --- a/client/loader/download.go +++ b/client/loader/download.go @@ -1,19 +1,84 @@ package loader import ( + "errors" + "fmt" "galaxy/connector" "galaxy/util" ) -func (l *loader) newerVersion(version string) bool { - return util.CompareSemver(util.MustParseSemver(l.client.Version()), util.MustParseSemver(version)) > 0 +func (l *loader) checkAndDownloadPluginVersion() error { + versions, err := l.connector.CheckVersion() + if err != nil { + return err + } else if latest, ok, err := latestVersion(versions); err != nil { + return err + } else if ok { + return l.preparePluginVersion(latest) + } else { + return errors.New("Server did not respond with a suitable client version") + } } -// downloadVersion fetches given version artifact, when newer to the current version, -// and stores at the App's local storage with a pre-defined name with semver suffix -func (l *loader) downloadVersion(v connector.VersionInfo) { - if !l.newerVersion(v.Version) { - return +// preparePluginVersion fetches and stores given version of plugin artifact, if newer to the current known version. +func (l *loader) preparePluginVersion(v connector.VersionInfo) error { + s, exists, err := l.loadCurrentState() + if err != nil { + return err } - l.connector.DownloadVersion(v.URL) + version, err := util.ParseSemver(v.Version) + if err != nil { + return err + } + if exists { + currentVersion, err := util.ParseSemver(s.ClientCurrentVersion) + if err != nil { + return err + } + if util.CompareSemver(currentVersion, version) > 0 { + return nil + } + } + err = l.downloadPlugin(v.URL, v.Version, v.Checksum) + if err != nil { + return err + } + return nil +} + +// downloadPlugin downloads and stores at the App's local storage with a pre-defined name with semver suffix. +func (l *loader) downloadPlugin(url, version string, checksum [32]byte) error { + data, err := l.connector.DownloadVersion(url) + if err != nil { + return fmt.Errorf("Plugin v%s download error: %w", version, err) + } + dataChecksum := SumSHA256(data) + if !EqualSHA256(checksum, dataChecksum) { + return errors.New("Downloaded pligin checksum incorrect") + } + versionFileName := resolvePluginFile(version) + versionFileExists, _, err := l.storage.FileExists(versionFileName) + if err != nil { + return fmt.Errorf("Check plugin v%s exists error: %w", version, err) + } + if versionFileExists { + storedData, err := l.storage.ReadFile(versionFileName) + if err != nil { + return err + } + if EqualSHA256(dataChecksum, SumSHA256(storedData)) { + return nil + } + if err := l.storage.DeleteFile(versionFileName); err != nil { + return fmt.Errorf("Plugin v%s delete error: %w", version, err) + } + } + err = l.storage.WriteFile(versionFileName, data) + if err != nil { + return fmt.Errorf("Plugin file write error: %w", err) + } + if err := l.setNextVersion(&version); err != nil { + return err + } + return nil } diff --git a/client/loader/loader.go b/client/loader/loader.go index ecb0654..24a519c 100644 --- a/client/loader/loader.go +++ b/client/loader/loader.go @@ -1,3 +1,11 @@ +/* +Пакет loader - основная точка входа в клиенское приложение. + +Задачи: + - Загрузка и выполнение плагина, в котором сосредоточена логика основного UI, + - Выполнение операций с локальным Storage, + - Выполнение операций обмена данными с сервером. +*/ package loader import ( @@ -64,50 +72,78 @@ func NewLoader(s storage.Storage, conn connector.Connector, app fyne.App) (*load return l, nil } +// updatePluginFromVersion заменяет файл {libUIPluginFile} с плагином на новую версию, которая была ранее загружена и сохранена в State +func (l *loader) updatePluginFromVersion() error { + state, stateExists, err := l.loadCurrentState() + if err != nil { + return fmt.Errorf("Load State error: %w", err) + } + if !stateExists { + return nil + } + if state.ClientNextVersion == nil { + return nil + } + version := *state.ClientNextVersion + versionFileName := resolvePluginFile(version) + versionFileExists, _, err := l.storage.FileExists(versionFileName) + if err != nil { + return fmt.Errorf("Check plugin v%s exists error: %w", version, err) + } + if !versionFileExists { + return fmt.Errorf("Requested plugin v%s does not exists at local storage", version) + } + storedData, err := l.storage.ReadFile(versionFileName) + if err != nil { + return err + } + if err := l.storage.WriteFile(libUIPluginFile, storedData); err != nil { + return fmt.Errorf("Plugin file write error: %w", err) + } + state.ClientCurrentVersion = version + state.ClientNextVersion = nil + if err := l.storage.SaveState(*state); err != nil { + return fmt.Errorf("State write error: %w", err) + } + return nil +} + func (l *loader) initPlugin() (mc.Client, error) { - exists, pluginPath, err := l.storage.FileExists(libUIPluginFile) + pluginExists, pluginPath, err := l.storage.FileExists(libUIPluginFile) if err != nil { l.appendFatalError(fmt.Errorf("Client plugin file lookup error: %w", err)) return nil, err } - if !exists { + + if !pluginExists { l.logText("Client plugin file not found, fetching available versions") - v, err := l.connector.CheckVersion() + err = l.checkAndDownloadPluginVersion() if err != nil { l.logError(err) return nil, err } - l.logText(fmt.Sprintf("Received %d versions", len(v))) - latest, ok, err := latestVersion(v) - if err != nil { - l.logError(err) - return nil, err - } - if !ok { - l.logError(errors.New("Server did not responded with a suitable client version")) - return nil, err - } - l.logText(fmt.Sprintf("Downloading version %s", latest.Version)) - data, err := l.connector.DownloadVersion(latest.URL) - if err != nil { - l.logError(fmt.Errorf("Version %s download error: %w", latest.Version, err)) - return nil, err - } - err = l.storage.WriteFile(libUIPluginFile, data) - if err != nil { - l.appendFatalError(fmt.Errorf("Plugin file write error: %w", err)) - return nil, err - } } + + err = l.updatePluginFromVersion() + if err != nil { + return nil, err + } + l.logText(fmt.Sprintf("Loading client plugin from %s", pluginPath)) cli, err := loadClientPlugin(l.storage, l.connector, l.app, pluginPath, pluginInitSymbol) if err != nil { l.appendFatalError(err) return nil, err } + + // need to store current plugin version for the very first app start + if err := l.updateCurrentVersion(cli.Version()); err != nil { + return nil, err + } return cli, nil } +// init инициализирует плагин клиента и запускает его, либо отображает пользователю лог с ошибками func (l *loader) init() { l.fatalError = false fyne.Do(func() { @@ -161,9 +197,15 @@ func (l *loader) appendFatalError(err error) { l.fatalError = true } +// Run is the main entry point for start Client func (l *loader) Run(ctx context.Context) error { + // start initializing process go l.init() + + // run UI engine, not the main Client app l.app.Run() + + // wait for successfull 'loaded' signal from init() select { case <-ctx.Done(): return nil @@ -172,50 +214,23 @@ func (l *loader) Run(ctx context.Context) error { } } +// runClient запусукает основного Client и блокирует выполнение в текущем потоке func (l *loader) runClient(ctx context.Context) error { if l.client == nil { return errors.New("run: client wasn't initialized, this is an program fatal error.") } - final := make(chan struct{}, 1) - go l.backgroundLoop(ctx, final) - defer func() { final <- struct{}{} }() + + bgSignal := make(chan struct{}, 1) + go l.backgroundLoop(ctx, bgSignal) + // stop backbround loop before exin func + defer func() { bgSignal <- struct{}{} }() + + // Client's Run() blocks execution until window is closed, or error is returned immediately if err := l.client.Run(); err != nil { return err } - final <- struct{}{} - return nil -} -func (l *loader) backgroundLoop(ctx context.Context, final <-chan struct{}) { - checkConnTimer := time.NewTimer(checkConnectionTimeout) - checkVersionTimer := time.NewTimer(checkVersionTimeout) - defer func() { - checkConnTimer.Stop() - checkVersionTimer.Stop() - }() - for { - select { - case <-ctx.Done(): - l.client.Shutdown() - return - case <-final: - return - case <-checkConnTimer.C: - isGood := l.connector.CheckConnection() - l.client.OnConnection(isGood) - checkConnTimer.Reset(checkConnectionTimeout) - case <-checkVersionTimer.C: - versions, err := l.connector.CheckVersion() - if err != nil { - // propagate error to the UI - } else if latest, ok, err := latestVersion(versions); err != nil { - // propagate error to the UI - } else if ok { - l.downloadVersion(latest) - } - checkVersionTimer.Reset(checkVersionTimeout) - } - } + return nil } // loadClientPlugin loads a Client implementation from a shared plugin file at the specified path. diff --git a/client/loader/service.go b/client/loader/service.go new file mode 100644 index 0000000..bad6783 --- /dev/null +++ b/client/loader/service.go @@ -0,0 +1,34 @@ +package loader + +import ( + "context" + "time" +) + +// backgroundLoop периодически проверяет доступность соединения и наличие новых версий на сервере +func (l *loader) backgroundLoop(ctx context.Context, stop <-chan struct{}) { + checkConnTimer := time.NewTimer(checkConnectionTimeout) + checkVersionTimer := time.NewTimer(checkVersionTimeout) + defer func() { + checkConnTimer.Stop() + checkVersionTimer.Stop() + }() + for { + select { + case <-ctx.Done(): + l.client.Shutdown() + return + case <-stop: + return + case <-checkConnTimer.C: + isGood := l.connector.CheckConnection() + l.client.OnConnection(isGood) + checkConnTimer.Reset(checkConnectionTimeout) + case <-checkVersionTimer.C: + if err := l.checkAndDownloadPluginVersion(); err != nil { + l.onConnectionError(err) // TODO: separate error types for: onConnectionError / onStorageError / onServiceError + } + checkVersionTimer.Reset(checkVersionTimeout) + } + } +} diff --git a/client/loader/state.go b/client/loader/state.go new file mode 100644 index 0000000..35af2ee --- /dev/null +++ b/client/loader/state.go @@ -0,0 +1,61 @@ +package loader + +import ( + "errors" + "fmt" + mc "galaxy/model/client" +) + +func (l *loader) loadCurrentState() (*mc.State, bool, error) { + stateExists, err := l.storage.StateExists() + if err != nil { + return nil, false, fmt.Errorf("State file lookup error: %w", err) + } + if !stateExists { + return nil, false, nil + } + if s, err := l.storage.LoadState(); err != nil { + return nil, false, fmt.Errorf("State read error: %w", err) + } else { + return &s, true, nil + } +} + +func (l *loader) updateCurrentVersion(v string) error { + state, stateExists, err := l.loadCurrentState() + if err != nil { + return fmt.Errorf("Load State error: %w", err) + } + if !stateExists { + state = &mc.State{} + } + if state.ClientCurrentVersion == v { + return nil + } + state.ClientCurrentVersion = v + if err := l.storage.SaveState(*state); err != nil { + return fmt.Errorf("State write error: %w", err) + } + return nil +} + +func (l *loader) setNextVersion(v *string) error { + state, stateExists, err := l.loadCurrentState() + if err != nil { + return fmt.Errorf("Load State error: %w", err) + } + if !stateExists { + return errors.New("State was never saved, unable to set next version") + } + if state.ClientNextVersion != nil && v != nil && *state.ClientNextVersion == *v { + return nil + } + if state.ClientNextVersion == v { + return nil + } + state.ClientNextVersion = v + if err := l.storage.SaveState(*state); err != nil { + return fmt.Errorf("State write error: %w", err) + } + return nil +} diff --git a/client/loader/util.go b/client/loader/util.go index 7cfc8b0..0f18904 100644 --- a/client/loader/util.go +++ b/client/loader/util.go @@ -1,6 +1,7 @@ package loader import ( + "crypto/sha256" "fmt" "galaxy/connector" "galaxy/util" @@ -12,7 +13,7 @@ func resolvePluginFile(version string) string { return libUIPluginFile + "-" + version } -// latestVersion should return VersionInfo with the latest Version for the current OD +// latestVersion should return VersionInfo with the latest Version for the current OS func latestVersion(versions []connector.VersionInfo) (connector.VersionInfo, bool, error) { os := runtime.GOOS versions = slices.DeleteFunc(versions, func(v connector.VersionInfo) bool { return v.OS != os }) @@ -37,3 +38,22 @@ func latestVersion(versions []connector.VersionInfo) (connector.VersionInfo, boo slices.SortFunc(semvers, func(a, b *v) int { return util.CompareSemver(*b.sv, *a.sv) }) return *semvers[0].vi, true, nil } + +// SumSHA256 calculates SHA-256 for the provided byte slice and returns +// the raw 32-byte digest as a fixed-size array. +// +// The function does not modify the input data. +// The returned value is the binary digest, not a hex string. +// Use digest[:] if a []byte is needed. +func SumSHA256(data []byte) [32]byte { + return sha256.Sum256(data) +} + +// EqualSHA256 returns true when both SHA-256 digests are identical. +// +// Since SHA-256 digest is represented as a fixed-size array [32]byte, +// Go allows direct value comparison with ==. +// This is the simplest and fastest approach for ordinary equality checks. +func EqualSHA256(a, b [32]byte) bool { + return a == b +} diff --git a/client/loader/util_test.go b/client/loader/util_test.go new file mode 100644 index 0000000..3f63a77 --- /dev/null +++ b/client/loader/util_test.go @@ -0,0 +1,56 @@ +package loader + +import ( + "crypto/sha256" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestSumSHA256 verifies that SumSHA256 returns the same digest +// as the standard library implementation for a non-empty payload. +func TestSumSHA256(t *testing.T) { + t.Parallel() + + data := []byte("hello world") + expected := sha256.Sum256(data) + + actual := SumSHA256(data) + + require.Equal(t, expected, actual) +} + +// TestSumSHA256Empty verifies that SumSHA256 correctly handles +// an empty byte slice. +func TestSumSHA256Empty(t *testing.T) { + t.Parallel() + + data := []byte{} + expected := sha256.Sum256(data) + + actual := SumSHA256(data) + + require.Equal(t, expected, actual) +} + +// TestEqualSHA256Same verifies that two identical digests +// are considered equal. +func TestEqualSHA256Same(t *testing.T) { + t.Parallel() + + data := []byte("hello") + digest := sha256.Sum256(data) + + require.True(t, EqualSHA256(digest, digest)) +} + +// TestEqualSHA256Different verifies that different digests +// are considered not equal. +func TestEqualSHA256Different(t *testing.T) { + t.Parallel() + + digestA := sha256.Sum256([]byte("hello")) + digestB := sha256.Sum256([]byte("world")) + + require.False(t, EqualSHA256(digestA, digestB)) +} diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 9f170ab..0237750 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -1,6 +1,10 @@ package connector import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" "galaxy/model/client" "galaxy/model/report" ) @@ -29,7 +33,72 @@ type UIConnector interface { } type VersionInfo struct { - OS string `json:"os"` // Operating System name (unix, darwin, windows, etc.) - Version string `json:"version"` // Semver format: X.Y.Z - URL string `json:"url"` // Artifact download URL for this version + OS string `json:"os"` // Operating System name (unix, darwin, windows, etc.) + Version string `json:"version"` // Semver format: X.Y.Z + URL string `json:"url"` // Artifact download URL for this version + Checksum SHA256Digest `json:"sha256"` // Base64 SHA-256 checksum for artifact binary data +} + +// SHA256Digest represents a SHA-256 digest in raw binary form. +// +// Internally it stores the exact 32-byte digest. +// In JSON it is encoded as a lowercase hexadecimal string of 64 characters. +type SHA256Digest [32]byte + +// NewSHA256Digest calculates SHA-256 for the provided byte slice +// and returns the digest as SHA256Digest. +// +// The function does not modify the input data. +func NewSHA256Digest(data []byte) SHA256Digest { + sum := sha256.Sum256(data) + return SHA256Digest(sum) +} + +// String returns the lowercase hexadecimal representation +// of the digest. +// +// The returned string always contains exactly 64 characters. +func (d SHA256Digest) String() string { + return hex.EncodeToString(d[:]) +} + +// MarshalJSON encodes the digest as a JSON string containing +// the lowercase hexadecimal SHA-256 value. +// +// Example JSON value: +// +// "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" +func (d SHA256Digest) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// UnmarshalJSON decodes a JSON string containing a lowercase or uppercase +// hexadecimal SHA-256 value into the digest. +// +// The input must be a JSON string with exactly 64 hexadecimal characters. +func (d *SHA256Digest) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("sha256 digest must be a JSON string: %w", err) + } + + if len(s) != hex.EncodedLen(len(d)) { + return fmt.Errorf("invalid SHA-256 hex length: got %d, want %d", len(s), hex.EncodedLen(len(d))) + } + + decoded, err := hex.DecodeString(s) + if err != nil { + return fmt.Errorf("invalid SHA-256 hex value: %w", err) + } + + copy(d[:], decoded) + return nil +} + +// Equal returns true when both digests are identical. +// +// Since SHA256Digest is based on a fixed-size array, direct value comparison +// is efficient and idiomatic for non-constant-time equality checks. +func (d SHA256Digest) Equal(other SHA256Digest) bool { + return d == other } diff --git a/pkg/connector/connector_test.go b/pkg/connector/connector_test.go new file mode 100644 index 0000000..6e286fa --- /dev/null +++ b/pkg/connector/connector_test.go @@ -0,0 +1,89 @@ +package connector + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestSHA256DigestMarshalJSON verifies that the digest is encoded +// as a lowercase hexadecimal JSON string. +func TestSHA256DigestMarshalJSON(t *testing.T) { + t.Parallel() + + digest := NewSHA256Digest([]byte("hello world")) + + data, err := json.Marshal(digest) + require.NoError(t, err) + + require.Equal(t, `"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"`, string(data)) +} + +// TestSHA256DigestUnmarshalJSON verifies that a valid hexadecimal JSON string +// is decoded back into the original digest. +func TestSHA256DigestUnmarshalJSON(t *testing.T) { + t.Parallel() + + var digest SHA256Digest + err := json.Unmarshal( + []byte(`"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"`), + &digest, + ) + require.NoError(t, err) + + expected := NewSHA256Digest([]byte("hello world")) + require.True(t, digest.Equal(expected)) +} + +// TestSHA256DigestUnmarshalJSONInvalidLength verifies that invalid digest length +// is rejected. +func TestSHA256DigestUnmarshalJSONInvalidLength(t *testing.T) { + t.Parallel() + + var digest SHA256Digest + err := json.Unmarshal([]byte(`"abcd"`), &digest) + + require.Error(t, err) + require.Contains(t, err.Error(), "invalid SHA-256 hex length") +} + +// TestSHA256DigestUnmarshalJSONInvalidHex verifies that non-hexadecimal input +// is rejected. +func TestSHA256DigestUnmarshalJSONInvalidHex(t *testing.T) { + t.Parallel() + + var digest SHA256Digest + err := json.Unmarshal( + []byte(`"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"`), + &digest, + ) + + require.Error(t, err) + require.Contains(t, err.Error(), "invalid SHA-256 hex value") +} + +// TestFileMetadataJSONRoundTrip verifies that a struct containing the digest +// round-trips correctly through JSON. +func TestFileMetadataJSONRoundTrip(t *testing.T) { + t.Parallel() + + original := VersionInfo{ + OS: "linux", + Version: "1.2.3", + URL: "http://server:8080", + Checksum: NewSHA256Digest([]byte("payload")), + } + + data, err := json.Marshal(original) + require.NoError(t, err) + + var decoded VersionInfo + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + require.Equal(t, original.OS, decoded.OS) + require.Equal(t, original.Version, decoded.Version) + require.Equal(t, original.URL, decoded.URL) + require.True(t, original.Checksum.Equal(decoded.Checksum)) +} diff --git a/pkg/model/client/client.go b/pkg/model/client/client.go index 63cb14d..925eed6 100644 --- a/pkg/model/client/client.go +++ b/pkg/model/client/client.go @@ -18,6 +18,15 @@ type Client interface { // OnConnection receives an event when connection with client's server may be established (true) or connectivity lost (false). OnConnection(bool) + + // OnConnectionError receives an event when background process catches an error related to the server connectivity. + OnConnectionError(error) + + // OnStorageError receives an event when background process catches an error related to the app's local Storage. + OnStorageError(error) + + // OnServiceError receives an event when background process catches an unexpected processing error. + OnServiceError(error) } type GameID string @@ -30,8 +39,8 @@ type State struct { // TODO: store user's login key ClientCurrentVersion string `json:"clientCurrentVersion"` ClientNextVersion *string `json:"clientNextVersion,omitempty"` - GameState []GameState `json:"gameState"` - ActiveGameID GameID `json:"activeGameId"` + GameState []GameState `json:"gameState,omitempty"` + ActiveGameID *GameID `json:"activeGameId,omitempty"` } type GameState struct { diff --git a/pkg/storage/fs/fs.go b/pkg/storage/fs/fs.go index 4708e69..a074e89 100644 --- a/pkg/storage/fs/fs.go +++ b/pkg/storage/fs/fs.go @@ -179,7 +179,7 @@ func (s *fsStorage) FileExists(path string) (bool, string, error) { return false, "", err } if !exists { - return false, "", nil + return false, absPath, nil } return true, absPath, nil } diff --git a/pkg/storage/fs/fs_test.go b/pkg/storage/fs/fs_test.go index ebec1a3..6068249 100644 --- a/pkg/storage/fs/fs_test.go +++ b/pkg/storage/fs/fs_test.go @@ -191,8 +191,9 @@ func TestRawFileCRUDAndList(t *testing.T) { if missingExists { t.Fatal("missing.txt should not exist") } - if missingPath != "" { - t.Fatalf("missing file path = %q, want empty string", missingPath) + wantMissingPath := filepath.Join(s.storageRoot, "missing.txt") + if wantMissingPath != missingPath { + t.Fatalf("missing file path = %q, want %q", missingPath, wantMissingPath) } alphaData, err := s.ReadFile("nested/alpha.txt") @@ -478,7 +479,7 @@ func sampleState() client.State { {ID: client.GameID("game-1"), LastTurn: 12, ActiveTurn: 11}, {ID: client.GameID("game-2"), LastTurn: 4, ActiveTurn: 4}, }, - ActiveGameID: client.GameID("game-2"), + ActiveGameID: new(client.GameID("game-2")), } }