package updates import ( "context" "errors" "io" "path/filepath" "sync" "testing" "time" ) func TestNormalizeVersion(t *testing.T) { t.Parallel() version, err := NormalizeVersion("1.2.3") if err != nil { t.Fatalf("NormalizeVersion returned error: %v", err) } if version != "v1.2.3" { t.Fatalf("NormalizeVersion returned %q", version) } } func TestControllerCheckAndDownload(t *testing.T) { t.Parallel() platform := &fakePlatform{ root: InstallRoot{Path: t.TempDir()}, stage: StagedArtifact{ Path: t.TempDir(), Root: InstallRoot{Path: t.TempDir()}, Bundle: BundleManifest{ SchemaVersion: 1, EntryPoint: "App", Files: []BundleFile{{Path: "App", Mode: "0755"}}, }, }, } release := &Release{ ID: "1.1.0", Version: "1.1.0", Channel: ChannelStable, Artifact: Artifact{ Kind: ArtifactKindBundleArchive, Format: ArtifactFormatZip, URL: "https://example.invalid/app.zip", SHA256: "deadbeef", }, } controller, err := NewController(Config{ App: AppDescriptor{ ProductID: "com.example.app", CurrentVersion: "1.0.0", Channel: ChannelStable, OS: "linux", Arch: "amd64", ExecutablePath: filepath.Join(t.TempDir(), "App"), }, Provider: fakeProvider{release: release}, Downloader: fakeDownloader{file: DownloadedFile{Path: "bundle.zip", SHA256: "deadbeef"}}, Store: &memoryStore{}, Platform: platform, }) if err != nil { t.Fatalf("NewController returned error: %v", err) } snapshot, err := controller.Check(context.Background(), CheckRequest{}) if err != nil { t.Fatalf("Check returned error: %v", err) } if snapshot.State != StateUpdateAvailable { t.Fatalf("Check state = %s", snapshot.State) } snapshot, err = controller.Download(context.Background()) if err != nil { t.Fatalf("Download returned error: %v", err) } if snapshot.State != StateReadyToApply { t.Fatalf("Download state = %s", snapshot.State) } if snapshot.Staged == nil { t.Fatal("Download did not stage artifact") } } func TestControllerBusy(t *testing.T) { t.Parallel() block := make(chan struct{}) controller, err := NewController(Config{ App: AppDescriptor{ ProductID: "com.example.app", CurrentVersion: "1.0.0", Channel: ChannelStable, OS: "linux", Arch: "amd64", ExecutablePath: filepath.Join(t.TempDir(), "App"), }, Provider: fakeProvider{ resolveFunc: func(context.Context, ResolveRequest) (*Release, error) { <-block return nil, nil }, }, Downloader: fakeDownloader{}, Store: &memoryStore{}, Platform: &fakePlatform{root: InstallRoot{Path: t.TempDir()}}, }) if err != nil { t.Fatalf("NewController returned error: %v", err) } done := make(chan error, 1) go func() { _, err := controller.Check(context.Background(), CheckRequest{}) done <- err }() time.Sleep(50 * time.Millisecond) if _, err := controller.Check(context.Background(), CheckRequest{}); !errors.Is(err, ErrBusy) { t.Fatalf("expected ErrBusy, got %v", err) } close(block) if err := <-done; err != nil { t.Fatalf("first Check returned error: %v", err) } } func TestControllerCleansStagedUpdateOnMatchingVersion(t *testing.T) { t.Parallel() store := &memoryStore{} err := store.Save(context.Background(), Snapshot{ State: StateRestarting, Staged: &StagedArtifact{ Release: Release{Version: "1.2.0"}, }, Candidate: &Release{Version: "1.2.0"}, }) if err != nil { t.Fatalf("Save returned error: %v", err) } controller, err := NewController(Config{ App: AppDescriptor{ ProductID: "com.example.app", CurrentVersion: "1.2.0", Channel: ChannelStable, OS: "linux", Arch: "amd64", ExecutablePath: filepath.Join(t.TempDir(), "App"), }, Provider: fakeProvider{}, Downloader: fakeDownloader{}, Store: store, Platform: &fakePlatform{root: InstallRoot{Path: t.TempDir()}}, }) if err != nil { t.Fatalf("NewController returned error: %v", err) } snapshot := controller.Snapshot() if snapshot.Staged != nil { t.Fatal("expected staged update to be cleared") } if snapshot.State != StateUpToDate { t.Fatalf("snapshot state = %s", snapshot.State) } } type fakeProvider struct { release *Release resolveFunc func(context.Context, ResolveRequest) (*Release, error) } func (provider fakeProvider) Resolve(ctx context.Context, req ResolveRequest) (*Release, error) { if provider.resolveFunc != nil { return provider.resolveFunc(ctx, req) } return provider.release, nil } func (provider fakeProvider) OpenArtifact(context.Context, Release) (io.ReadCloser, error) { return nil, nil } type fakeDownloader struct { file DownloadedFile err error } func (downloader fakeDownloader) Download(context.Context, Artifact) (DownloadedFile, error) { return downloader.file, downloader.err } type fakePlatform struct { root InstallRoot stage StagedArtifact err error } func (platform *fakePlatform) DetectInstallRoot(AppDescriptor) (InstallRoot, error) { return platform.root, platform.err } func (platform *fakePlatform) Stage(context.Context, InstallRoot, DownloadedFile, Release) (StagedArtifact, error) { return platform.stage, platform.err } func (platform *fakePlatform) SpawnApplyAndRestart(context.Context, ApplyRequest) error { return platform.err } type memoryStore struct { mu sync.Mutex snapshot Snapshot } func (store *memoryStore) Load(context.Context) (Snapshot, error) { store.mu.Lock() defer store.mu.Unlock() return store.snapshot, nil } func (store *memoryStore) Save(_ context.Context, snapshot Snapshot) error { store.mu.Lock() defer store.mu.Unlock() store.snapshot = snapshot return nil } func (store *memoryStore) ClearStaged(context.Context) error { store.mu.Lock() defer store.mu.Unlock() store.snapshot.Staged = nil store.snapshot.Candidate = nil return nil }