Files
silo-server/internal/plugins/auto_update_replace_test.go

469 lines
14 KiB
Go

package plugins
import (
"context"
"testing"
pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1"
)
func TestAutoUpdateServiceCheckReplacesInstalledPluginsInPlace(t *testing.T) {
installations := &fakeAutoUpdateInstallations{
list: []*Installation{
{ID: 41, RepositoryID: pluginRepositoryID(7), PluginID: "silo.tmdb", Version: "1.0.0", UpdatePolicy: "auto", Enabled: true},
},
}
host := &fakeAutoUpdateHost{}
installer := &fakeAutoUpdateInstaller{}
catalog := &fakeAutoUpdateCatalog{
entries: []CatalogEntry{
{
RepositoryID: 7,
Manifest: &pluginv1.PluginManifest{
PluginId: "silo.tmdb",
Version: "1.1.0",
},
},
},
resolved: &ResolvedCatalogInstall{
RepositoryID: 7,
ArchiveURL: "https://plugins.example.test/tmdb",
Checksum: "deadbeef",
},
}
service := NewAutoUpdateService(
&fakeAutoUpdateRepositories{list: []*Repository{{ID: 7, Enabled: true}}},
installations,
catalog,
installer,
host,
nil,
nil,
)
summary, err := service.Check(context.Background(), AutoUpdateOptions{
SeedDefaultRepository: true,
AutoInstallDefaults: false,
})
if err != nil {
t.Fatalf("Check() returned error: %v", err)
}
if summary.UpdatesApplied != 1 {
t.Fatalf("UpdatesApplied = %d, want 1", summary.UpdatesApplied)
}
if len(installations.deletedIDs) != 0 {
t.Fatalf("deleted installation IDs = %#v, want none", installations.deletedIDs)
}
if len(installer.replaceBinary) != 1 {
t.Fatalf("replace binary calls = %d, want 1", len(installer.replaceBinary))
}
if installer.replaceBinary[0].existingID != 41 {
t.Fatalf("replace binary existing_id = %d, want 41", installer.replaceBinary[0].existingID)
}
if len(host.stopped) != 1 || host.stopped[0] != 41 {
t.Fatalf("stopped installations = %#v, want [41]", host.stopped)
}
}
func TestAutoUpdateServiceCheckMatchesRenamedRepositoryByStablePluginIdentity(t *testing.T) {
installations := &fakeAutoUpdateInstallations{
list: []*Installation{
{ID: 41, RepositoryID: pluginRepositoryID(7), PluginID: "silo.tmdb", Version: "1.0.0", UpdatePolicy: "auto", Enabled: true},
},
}
installer := &fakeAutoUpdateInstaller{}
catalog := &fakeAutoUpdateCatalog{
entries: []CatalogEntry{
{
RepositoryID: 7,
RepoURL: "https://github.com/Silo-Server/silo-plugin-metadata-tmdb",
Manifest: &pluginv1.PluginManifest{
PluginId: "silo.tmdb",
Version: "1.1.0",
},
},
},
resolved: &ResolvedCatalogInstall{
RepositoryID: 7,
ArchiveURL: "https://github.com/Silo-Server/silo-plugin-metadata-tmdb/releases/download/v1.1.0/plugin-linux-amd64",
Checksum: "deadbeef",
},
}
service := NewAutoUpdateService(
&fakeAutoUpdateRepositories{list: []*Repository{{
ID: 7,
URL: DefaultRepositoryURL,
Enabled: true,
}}},
installations,
catalog,
installer,
&fakeAutoUpdateHost{},
nil,
nil,
)
summary, err := service.Check(context.Background(), AutoUpdateOptions{})
if err != nil {
t.Fatalf("Check() returned error: %v", err)
}
if summary.UpdatesApplied != 1 {
t.Fatalf("UpdatesApplied = %d, want 1", summary.UpdatesApplied)
}
if len(installer.replaceBinary) != 1 || installer.replaceBinary[0].existingID != 41 {
t.Fatalf("replace binary calls = %#v, want one in-place replacement for installation 41", installer.replaceBinary)
}
if len(installations.deletedIDs) != 0 {
t.Fatalf("deleted installation IDs = %#v, want none", installations.deletedIDs)
}
}
func TestAutoUpdateServiceCheckFiresOnChangeAfterMutation(t *testing.T) {
newOnChangeService := func(installations *fakeAutoUpdateInstallations, opts AutoUpdateOptions, calls *int) (AutoUpdateSummary, error) {
catalog := &fakeAutoUpdateCatalog{
entries: []CatalogEntry{{
RepositoryID: 7,
Manifest: &pluginv1.PluginManifest{
PluginId: "silo.tmdb",
Version: "1.1.0",
},
}},
resolved: &ResolvedCatalogInstall{
RepositoryID: 7,
ArchiveURL: "https://plugins.example.test/tmdb",
Checksum: "deadbeef",
},
}
service := NewAutoUpdateService(
&fakeAutoUpdateRepositories{list: []*Repository{{ID: 7, Enabled: true}}},
installations,
catalog,
&fakeAutoUpdateInstaller{},
&fakeAutoUpdateHost{},
nil,
func(context.Context) { *calls++ },
)
return service.Check(context.Background(), opts)
}
t.Run("applied auto-update fires onChange", func(t *testing.T) {
var calls int
installations := &fakeAutoUpdateInstallations{
list: []*Installation{{ID: 41, RepositoryID: pluginRepositoryID(7), PluginID: "silo.tmdb", Version: "1.0.0", UpdatePolicy: "auto", Enabled: true}},
}
summary, err := newOnChangeService(installations, AutoUpdateOptions{}, &calls)
if err != nil {
t.Fatalf("Check() returned error: %v", err)
}
if summary.UpdatesApplied != 1 {
t.Fatalf("UpdatesApplied = %d, want 1", summary.UpdatesApplied)
}
if calls != 1 {
t.Fatalf("onChange calls = %d, want 1", calls)
}
})
t.Run("notify update fires onChange", func(t *testing.T) {
var calls int
installations := &fakeAutoUpdateInstallations{
list: []*Installation{{ID: 42, RepositoryID: pluginRepositoryID(7), PluginID: "silo.tmdb", Version: "1.0.0", UpdatePolicy: "notify", Enabled: true}},
}
summary, err := newOnChangeService(installations, AutoUpdateOptions{}, &calls)
if err != nil {
t.Fatalf("Check() returned error: %v", err)
}
if summary.UpdatesAvailable != 1 {
t.Fatalf("UpdatesAvailable = %d, want 1", summary.UpdatesAvailable)
}
if calls != 1 {
t.Fatalf("onChange calls = %d, want 1", calls)
}
})
t.Run("no mutation does not fire onChange", func(t *testing.T) {
var calls int
installations := &fakeAutoUpdateInstallations{
list: []*Installation{{ID: 43, RepositoryID: pluginRepositoryID(7), PluginID: "silo.tmdb", Version: "1.1.0", UpdatePolicy: "auto", Enabled: true}},
}
summary, err := newOnChangeService(installations, AutoUpdateOptions{}, &calls)
if err != nil {
t.Fatalf("Check() returned error: %v", err)
}
if summary.UpdatesApplied != 0 || summary.UpdatesAvailable != 0 {
t.Fatalf("unexpected mutation summary: %+v", summary)
}
if calls != 0 {
t.Fatalf("onChange calls = %d, want 0", calls)
}
})
t.Run("nil onChange is safe after mutation", func(t *testing.T) {
installations := &fakeAutoUpdateInstallations{
list: []*Installation{{ID: 44, RepositoryID: pluginRepositoryID(7), PluginID: "silo.tmdb", Version: "1.0.0", UpdatePolicy: "auto", Enabled: true}},
}
service := NewAutoUpdateService(
&fakeAutoUpdateRepositories{list: []*Repository{{ID: 7, Enabled: true}}},
installations,
&fakeAutoUpdateCatalog{
entries: []CatalogEntry{{
RepositoryID: 7,
Manifest: &pluginv1.PluginManifest{PluginId: "silo.tmdb", Version: "1.1.0"},
}},
resolved: &ResolvedCatalogInstall{RepositoryID: 7, ArchiveURL: "https://plugins.example.test/tmdb", Checksum: "deadbeef"},
},
&fakeAutoUpdateInstaller{},
&fakeAutoUpdateHost{},
nil,
nil,
)
summary, err := service.Check(context.Background(), AutoUpdateOptions{})
if err != nil {
t.Fatalf("Check() with nil onChange returned error: %v", err)
}
if summary.UpdatesApplied != 1 {
t.Fatalf("UpdatesApplied = %d, want 1", summary.UpdatesApplied)
}
})
}
func TestCompareVersions(t *testing.T) {
tests := []struct {
a, b string
want int
}{
{"1.2.9", "1.2.9", 0},
{"1.2.10", "1.2.9", 1},
{"1.2.9", "1.2.10", -1},
{"2.0.0", "1.9.9", 1},
{"1.0.0", "1.1.0", -1},
{"1.10.0", "1.9.0", 1},
{"0.0.30", "0.0.29", 1},
{"1.2.3", "1.2.3.1", -1},
}
for _, tt := range tests {
got := compareVersions(tt.a, tt.b)
if got != tt.want {
t.Errorf("compareVersions(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want)
}
}
}
func TestAutoUpdateMultiDigitVersion(t *testing.T) {
installations := &fakeAutoUpdateInstallations{
list: []*Installation{{ID: 50, RepositoryID: pluginRepositoryID(7), PluginID: "silo.tmdb", Version: "1.2.9", UpdatePolicy: "auto", Enabled: true}},
}
host := &fakeAutoUpdateHost{}
installer := &fakeAutoUpdateInstaller{}
catalog := &fakeAutoUpdateCatalog{
entries: []CatalogEntry{{
RepositoryID: 7,
Manifest: &pluginv1.PluginManifest{
PluginId: "silo.tmdb",
Version: "1.2.10",
},
}},
resolved: &ResolvedCatalogInstall{
RepositoryID: 7,
ArchiveURL: "https://plugins.example.test/tmdb",
Checksum: "deadbeef",
},
}
service := NewAutoUpdateService(
&fakeAutoUpdateRepositories{list: []*Repository{{ID: 7, Enabled: true}}},
installations,
catalog,
installer,
host,
nil,
nil,
)
summary, err := service.Check(context.Background(), AutoUpdateOptions{
SeedDefaultRepository: true,
AutoInstallDefaults: false,
})
if err != nil {
t.Fatalf("Check() returned error: %v", err)
}
if summary.UpdatesApplied != 1 {
t.Fatalf("UpdatesApplied = %d, want 1 (1.2.10 should be newer than 1.2.9)", summary.UpdatesApplied)
}
}
func TestAutoUpdateServiceKeepsUpdatesPinnedToInstallationRepository(t *testing.T) {
installations := &fakeAutoUpdateInstallations{
list: []*Installation{{
ID: 60,
RepositoryID: pluginRepositoryID(7),
PluginID: "silo.requests.arr",
Version: "1.0.0",
UpdatePolicy: "auto",
Enabled: true,
}},
}
catalog := &fakeAutoUpdateCatalog{
entries: []CatalogEntry{
{RepositoryID: 7, Manifest: &pluginv1.PluginManifest{PluginId: "silo.requests.arr", Version: "1.1.0"}},
{RepositoryID: 8, Manifest: &pluginv1.PluginManifest{PluginId: "silo.requests.arr", Version: "9.0.0"}},
},
resolved: &ResolvedCatalogInstall{RepositoryID: 7, ArchiveURL: "https://plugins.example.test/arr", Checksum: "deadbeef"},
}
service := NewAutoUpdateService(
&fakeAutoUpdateRepositories{list: []*Repository{{ID: 7, Enabled: true}, {ID: 8, Enabled: true}}},
installations,
catalog,
&fakeAutoUpdateInstaller{},
&fakeAutoUpdateHost{},
nil,
nil,
)
summary, err := service.Check(context.Background(), AutoUpdateOptions{})
if err != nil {
t.Fatalf("Check() returned error: %v", err)
}
if summary.UpdatesApplied != 1 {
t.Fatalf("UpdatesApplied = %d, want 1", summary.UpdatesApplied)
}
if len(catalog.resolveRequests) != 1 {
t.Fatalf("resolve requests = %d, want 1", len(catalog.resolveRequests))
}
request := catalog.resolveRequests[0]
if request.RepositoryID != 7 || request.Version != "1.1.0" {
t.Fatalf("resolved repository/version = %d/%s, want 7/1.1.0", request.RepositoryID, request.Version)
}
}
func TestAutoUpdateServiceDoesNotAttachUploadsToCatalogRepositories(t *testing.T) {
service := NewAutoUpdateService(
&fakeAutoUpdateRepositories{list: []*Repository{{ID: 7, Enabled: true}}},
&fakeAutoUpdateInstallations{list: []*Installation{{
ID: 61,
PluginID: "silo.requests.arr",
Version: "1.0.0",
UpdatePolicy: "auto",
Enabled: true,
}}},
&fakeAutoUpdateCatalog{entries: []CatalogEntry{{
RepositoryID: 7,
Manifest: &pluginv1.PluginManifest{PluginId: "silo.requests.arr", Version: "2.0.0"},
}}},
&fakeAutoUpdateInstaller{},
&fakeAutoUpdateHost{},
nil,
nil,
)
summary, err := service.Check(context.Background(), AutoUpdateOptions{})
if err != nil {
t.Fatalf("Check() returned error: %v", err)
}
if summary.UpdatesApplied != 0 || summary.UpdatesAvailable != 0 {
t.Fatalf("upload unexpectedly received catalog update: %+v", summary)
}
}
type fakeAutoUpdateRepositories struct {
list []*Repository
}
func (f *fakeAutoUpdateRepositories) List(context.Context) ([]*Repository, error) {
return f.list, nil
}
func (f *fakeAutoUpdateRepositories) Create(_ context.Context, input CreateRepositoryInput) (*Repository, error) {
repo := &Repository{
ID: len(f.list) + 1,
URL: input.URL,
DisplayName: input.DisplayName,
Enabled: input.Enabled == nil || *input.Enabled,
}
f.list = append(f.list, repo)
return repo, nil
}
type fakeAutoUpdateInstallations struct {
list []*Installation
updates []UpdateInstallationInput
deletedIDs []int
}
func (f *fakeAutoUpdateInstallations) List(context.Context) ([]*Installation, error) {
return f.list, nil
}
func (f *fakeAutoUpdateInstallations) Update(_ context.Context, _ int, input UpdateInstallationInput) error {
f.updates = append(f.updates, input)
return nil
}
func (f *fakeAutoUpdateInstallations) Delete(_ context.Context, id int) error {
f.deletedIDs = append(f.deletedIDs, id)
return nil
}
type fakeAutoUpdateCatalog struct {
entries []CatalogEntry
resolved *ResolvedCatalogInstall
resolveRequests []InstallCatalogRequest
}
func (f *fakeAutoUpdateCatalog) Fetch(context.Context) ([]CatalogEntry, error) {
return f.entries, nil
}
func (f *fakeAutoUpdateCatalog) ResolveInstall(_ context.Context, request InstallCatalogRequest) (*ResolvedCatalogInstall, error) {
f.resolveRequests = append(f.resolveRequests, request)
return f.resolved, nil
}
func pluginRepositoryID(id int) *int {
return &id
}
type replaceBinaryCall struct {
existingID int
req InstallBinaryRequest
}
type replaceRemoteCall struct {
existingID int
req InstallArchiveRequest
}
type fakeAutoUpdateInstaller struct {
replaceBinary []replaceBinaryCall
replaceRemote []replaceRemoteCall
binary []InstallBinaryRequest
remote []InstallArchiveRequest
}
func (f *fakeAutoUpdateInstaller) InstallRemote(_ context.Context, req InstallArchiveRequest) (*InstallResult, error) {
f.remote = append(f.remote, req)
return &InstallResult{}, nil
}
func (f *fakeAutoUpdateInstaller) InstallBinary(_ context.Context, req InstallBinaryRequest) (*InstallResult, error) {
f.binary = append(f.binary, req)
return &InstallResult{}, nil
}
func (f *fakeAutoUpdateInstaller) ReplaceRemote(_ context.Context, existing *Installation, req InstallArchiveRequest) (*InstallResult, error) {
f.replaceRemote = append(f.replaceRemote, replaceRemoteCall{existingID: existing.ID, req: req})
return &InstallResult{Installation: existing}, nil
}
func (f *fakeAutoUpdateInstaller) ReplaceBinary(_ context.Context, existing *Installation, req InstallBinaryRequest) (*InstallResult, error) {
f.replaceBinary = append(f.replaceBinary, replaceBinaryCall{existingID: existing.ID, req: req})
return &InstallResult{Installation: existing}, nil
}
type fakeAutoUpdateHost struct {
stopped []int
}
func (f *fakeAutoUpdateHost) Stop(installationID int) error {
f.stopped = append(f.stopped, installationID)
return nil
}