214 lines
8.0 KiB
Go
214 lines
8.0 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
package app
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/tentacle"
|
|
)
|
|
|
|
type installTransport func(*http.Request) (*http.Response, error)
|
|
|
|
func (transport installTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
return transport(request)
|
|
}
|
|
|
|
func install_fixture(t *testing.T) (tentacle.Repository, []byte) {
|
|
t.Helper()
|
|
program, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
binary, err := os.ReadFile(program)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
marker := filepath.Join(t.TempDir(), "called")
|
|
manifest := fmt.Sprintf("format-version=1\nid='echo'\nname='Echo'\nversion='0.1'\nruntime='native'\nplatforms=['linux']\nentry-point=['bin/echo','-test.run=^TestTentacleReaderPeer$','--','reader-peer',%q]\napis=['commands']\ncapabilities=[]\n[protocol]\nmajor=1\nminor=0\n", marker)
|
|
var data bytes.Buffer
|
|
compressed := gzip.NewWriter(&data)
|
|
archive := tar.NewWriter(compressed)
|
|
for _, item := range []struct {
|
|
name string
|
|
data []byte
|
|
mode int64
|
|
}{{"tentacle.toml", []byte(manifest), 0644}, {"bin/echo", binary, 0755}} {
|
|
if err := archive.WriteHeader(&tar.Header{Name: item.name, Size: int64(len(item.data)), Mode: item.mode}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := archive.Write(item.data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := archive.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := compressed.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(data.Bytes())
|
|
repository := tentacle.Repository{URL: "https://example.org/team/tentacles", ID: "example.tentacles", Name: "Example", Revision: strings.Repeat("a", 40), Tentacles: []tentacle.Manifest{{FormatVersion: 1, ID: "echo", Name: "Echo", Version: "0.1", Releases: []tentacle.Release{{Platform: "linux", Architecture: runtime.GOARCH, URL: "https://example.org/release.tar.gz", SHA256: fmt.Sprintf("%x", digest)}}}}}
|
|
return repository, data.Bytes()
|
|
}
|
|
|
|
func set_install_transport(t *testing.T, transport installTransport) {
|
|
t.Helper()
|
|
previous := http.DefaultTransport
|
|
http.DefaultTransport = transport
|
|
t.Cleanup(func() { http.DefaultTransport = previous })
|
|
}
|
|
|
|
func TestRepositoryPackageInstallEnableDisableRemoveAndOfflineRestart(t *testing.T) {
|
|
test_repository_package_install(t, false)
|
|
}
|
|
|
|
func TestNativeRepositoryPackageInstall(t *testing.T) {
|
|
if os.Getenv("CTHULHU_TENTACLE_MANAGED") != "1" {
|
|
t.Skip("requires a systemd user manager and cgroup v2")
|
|
}
|
|
test_repository_package_install(t, true)
|
|
}
|
|
|
|
func test_repository_package_install(t *testing.T, contained bool) {
|
|
repository, data := install_fixture(t)
|
|
downloads := 0
|
|
set_install_transport(t, func(*http.Request) (*http.Response, error) {
|
|
downloads++
|
|
return &http.Response{StatusCode: 200, ContentLength: int64(len(data)), Body: io.NopCloser(bytes.NewReader(data)), Header: make(http.Header)}, nil
|
|
})
|
|
registryPath := filepath.Join(t.TempDir(), "tentacles.toml")
|
|
owner, bindings := managed_tentacle_fixture(t, registryPath, contained)
|
|
if binary := os.Getenv("CTHULHU_TENTACLE_GUARDIAN_BINARY"); contained && binary != "" {
|
|
owner.guardianProgram = []string{binary, "--tentacle-guardian"}
|
|
}
|
|
owner.fetchRepository = func(context.Context, string) (tentacle.Repository, error) { return repository, nil }
|
|
if err := owner.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := owner.Manage(context.Background(), tentacle.ManagementRequest{Action: "repository-add", URL: repository.URL}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := owner.Manage(context.Background(), tentacle.ManagementRequest{Action: "install", ID: "echo", URL: repository.URL}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id := tentacle.PackageID(repository, "echo")
|
|
wait_managed_state(t, owner, id, "disabled")
|
|
entry := owner.Snapshot().Entries[0].Entry
|
|
if entry.Enabled || downloads != 1 {
|
|
t.Fatal(entry, downloads)
|
|
}
|
|
if err := owner.Manage(context.Background(), tentacle.ManagementRequest{Action: "install-enable", ID: "echo", URL: repository.URL}); err == nil {
|
|
t.Fatal("duplicate install accepted")
|
|
}
|
|
manage_tentacle(t, owner, "enable", id, nil)
|
|
wait_managed_state(t, owner, id, "active")
|
|
owner.Invoke(context.Background(), bindings.TentacleBindings()[0].Command)
|
|
waitTentacle(t, func() bool { _, err := os.Stat(entry.Program[len(entry.Program)-1]); return err == nil })
|
|
manage_tentacle(t, owner, "disable", id, nil)
|
|
wait_managed_state(t, owner, id, "disabled")
|
|
if _, err := os.Stat(entry.Program[0]); err != nil {
|
|
t.Fatal("disable deleted package", err)
|
|
}
|
|
manage_tentacle(t, owner, "enable", id, nil)
|
|
wait_managed_state(t, owner, id, "active")
|
|
shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := owner.Stop(shutdown); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restarted, _ := managed_tentacle_fixture(t, registryPath, contained)
|
|
if binary := os.Getenv("CTHULHU_TENTACLE_GUARDIAN_BINARY"); contained && binary != "" {
|
|
restarted.guardianProgram = []string{binary, "--tentacle-guardian"}
|
|
}
|
|
restarted.fetchRepository = func(context.Context, string) (tentacle.Repository, error) {
|
|
t.Error("restart fetched catalog")
|
|
return repository, nil
|
|
}
|
|
if err := restarted.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wait_managed_state(t, restarted, id, "active")
|
|
if downloads != 1 {
|
|
t.Fatal("enable/restart downloaded update", downloads)
|
|
}
|
|
if err := restarted.Manage(context.Background(), tentacle.ManagementRequest{Action: "repository-remove", URL: repository.URL}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(entry.Program[0]); err != nil {
|
|
t.Fatal("source removal deleted package", err)
|
|
}
|
|
manage_tentacle(t, restarted, "remove", id, nil)
|
|
waitTentacle(t, func() bool { _, err := os.Stat(entry.PackageDirectory); return os.IsNotExist(err) })
|
|
disk, err := (&tentacle.Registry{Path: registryPath}).Load()
|
|
if err != nil || len(disk) != 0 {
|
|
t.Fatal(disk, err)
|
|
}
|
|
}
|
|
|
|
func TestRepositoryPackageInstallFailureCancellationAndChangedCatalog(t *testing.T) {
|
|
repository, data := install_fixture(t)
|
|
for _, problem := range []string{"save failure", "cancel during download", "catalog changed", "emergency disable"} {
|
|
t.Run(problem, func(t *testing.T) {
|
|
registryPath := filepath.Join(t.TempDir(), "tentacles.toml")
|
|
registry := &tentacle.Registry{Path: registryPath}
|
|
if _, err := registry.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := registry.SaveState(nil, []tentacle.Repository{repository}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
owner, _ := managed_tentacle_fixture(t, registryPath, false)
|
|
if err := owner.Start(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
waitTentacle(t, func() bool { return len(owner.Snapshot().Repositories) == 1 })
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
set_install_transport(t, func(request *http.Request) (*http.Response, error) {
|
|
switch problem {
|
|
case "save failure":
|
|
if err := os.WriteFile(registryPath, []byte("# external edit\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "cancel during download":
|
|
cancel()
|
|
case "catalog changed":
|
|
if err := owner.Manage(context.Background(), tentacle.ManagementRequest{Action: "repository-remove", URL: repository.URL}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "emergency disable":
|
|
owner.Disable()
|
|
}
|
|
return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(data)), Header: make(http.Header)}, nil
|
|
})
|
|
if err := owner.Manage(ctx, tentacle.ManagementRequest{Action: "install-enable", ID: "echo", URL: repository.URL}); err == nil {
|
|
t.Fatal("failed installation accepted")
|
|
}
|
|
if len(owner.Snapshot().Entries) != 0 {
|
|
t.Fatal("failed installation registered entry")
|
|
}
|
|
files, err := os.ReadDir(tentacle.PackageRoot(registryPath))
|
|
if !os.IsNotExist(err) && err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(files) != 0 {
|
|
t.Fatal("failed installation left files", files)
|
|
}
|
|
})
|
|
}
|
|
}
|