feat: initial commit

This commit is contained in:
eric
2026-03-12 22:16:34 +01:00
parent 8555b02752
commit f13f4a9a69
155 changed files with 11988 additions and 0 deletions

View File

@@ -0,0 +1,242 @@
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
}