add test for the wall app

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-04-25 19:37:17 +02:00
commit 9e287e4e6a
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
15 changed files with 3022 additions and 1 deletions

355
internal/core/repo_test.go Normal file
View file

@ -0,0 +1,355 @@
package core_test
import (
"context"
"encoding/json"
"errors"
"testing"
aptclone "github.com/syonad/clonepack/internal/clone/apt"
"github.com/syonad/clonepack/internal/core"
"github.com/syonad/clonepack/internal/store"
)
// mockRepoStore is an in-memory implementation of store.RepoStore for tests.
type mockRepoStore struct {
repos map[int64]*store.Repo
nextID int64
}
func newMockRepoStore() *mockRepoStore {
return &mockRepoStore{repos: make(map[int64]*store.Repo), nextID: 1}
}
func (m *mockRepoStore) CreateRepo(_ context.Context, r *store.Repo) (int64, error) {
for _, existing := range m.repos {
if existing.Name == r.Name {
return 0, errors.New("UNIQUE constraint failed: repos.name")
}
}
id := m.nextID
m.nextID++
clone := *r
clone.ID = id
m.repos[id] = &clone
return id, nil
}
func (m *mockRepoStore) GetRepo(_ context.Context, id int64) (*store.Repo, error) {
r, ok := m.repos[id]
if !ok {
return nil, store.ErrNotFound
}
clone := *r
return &clone, nil
}
func (m *mockRepoStore) GetRepoByName(_ context.Context, name string) (*store.Repo, error) {
for _, r := range m.repos {
if r.Name == name {
clone := *r
return &clone, nil
}
}
return nil, store.ErrNotFound
}
func (m *mockRepoStore) ListRepos(_ context.Context) ([]store.Repo, error) {
out := make([]store.Repo, 0, len(m.repos))
for _, r := range m.repos {
out = append(out, *r)
}
return out, nil
}
func (m *mockRepoStore) DeleteRepo(_ context.Context, id int64) error {
if _, ok := m.repos[id]; !ok {
return store.ErrNotFound
}
delete(m.repos, id)
return nil
}
func (m *mockRepoStore) UpdateRepoSyncMode(_ context.Context, id int64, mode string) error {
r, ok := m.repos[id]
if !ok {
return store.ErrNotFound
}
r.SyncMode = mode
return nil
}
func (m *mockRepoStore) UpdateRepo(_ context.Context, id int64, sourceURL, syncMode, config string) error {
r, ok := m.repos[id]
if !ok {
return store.ErrNotFound
}
r.SourceURL = sourceURL
r.SyncMode = syncMode
r.Config = config
return nil
}
func newTestRepoService(t *testing.T) *core.RepoService {
t.Helper()
return core.NewRepoService(newMockRepoStore(), t.TempDir())
}
// --- Create tests ---
func TestCreate_validRPM(t *testing.T) {
svc := newTestRepoService(t)
repo, err := svc.Create(context.Background(), core.CreateRepoInput{
Name: "rocky9",
Type: "rpm",
SourceURL: "https://download.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if repo.ID <= 0 {
t.Errorf("expected positive ID, got %d", repo.ID)
}
if repo.SyncMode != "auto" {
t.Errorf("default SyncMode: got %q, want auto", repo.SyncMode)
}
}
func TestCreate_validAPT(t *testing.T) {
svc := newTestRepoService(t)
repo, err := svc.Create(context.Background(), core.CreateRepoInput{
Name: "debian-bookworm",
Type: "apt",
SourceURL: "https://deb.debian.org/debian",
AptSuite: "bookworm",
AptComponents: []string{"main", "contrib"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Config should be valid JSON with suite set.
var cfg aptclone.Config
if err := json.Unmarshal([]byte(repo.Config), &cfg); err != nil {
t.Fatalf("Config is not valid JSON: %v", err)
}
if cfg.Suite != "bookworm" {
t.Errorf("cfg.Suite: got %q, want bookworm", cfg.Suite)
}
if len(cfg.Components) != 2 {
t.Errorf("cfg.Components: got %v, want [main contrib]", cfg.Components)
}
}
func TestCreate_aptDefaultsComponents(t *testing.T) {
svc := newTestRepoService(t)
repo, err := svc.Create(context.Background(), core.CreateRepoInput{
Name: "debian-minimal",
Type: "apt",
SourceURL: "https://deb.debian.org/debian",
AptSuite: "bookworm",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var cfg aptclone.Config
json.Unmarshal([]byte(repo.Config), &cfg)
if len(cfg.Components) == 0 {
t.Error("expected default components, got empty")
}
if len(cfg.Architectures) == 0 {
t.Error("expected default architectures, got empty")
}
}
func TestCreate_missingName(t *testing.T) {
svc := newTestRepoService(t)
_, err := svc.Create(context.Background(), core.CreateRepoInput{
Type: "rpm",
SourceURL: "https://example.com",
})
if err == nil {
t.Error("expected error for missing name, got nil")
}
}
func TestCreate_invalidType(t *testing.T) {
svc := newTestRepoService(t)
_, err := svc.Create(context.Background(), core.CreateRepoInput{
Name: "bad-type",
Type: "maven",
SourceURL: "https://example.com",
})
if err == nil {
t.Error("expected error for invalid type, got nil")
}
}
func TestCreate_missingSourceURL(t *testing.T) {
svc := newTestRepoService(t)
_, err := svc.Create(context.Background(), core.CreateRepoInput{
Name: "no-url",
Type: "rpm",
})
if err == nil {
t.Error("expected error for missing source_url, got nil")
}
}
func TestCreate_aptMissingSuite(t *testing.T) {
svc := newTestRepoService(t)
_, err := svc.Create(context.Background(), core.CreateRepoInput{
Name: "apt-no-suite",
Type: "apt",
SourceURL: "https://deb.debian.org/debian",
})
if err == nil {
t.Error("expected error for missing apt_suite, got nil")
}
}
func TestCreate_invalidSyncMode(t *testing.T) {
svc := newTestRepoService(t)
_, err := svc.Create(context.Background(), core.CreateRepoInput{
Name: "bad-mode",
Type: "rpm",
SourceURL: "https://example.com",
SyncMode: "cron",
})
if err == nil {
t.Error("expected error for invalid sync_mode, got nil")
}
}
// --- Update tests ---
func TestUpdate_changeSourceURL(t *testing.T) {
svc := newTestRepoService(t)
ctx := context.Background()
repo, _ := svc.Create(ctx, core.CreateRepoInput{
Name: "rpm-repo",
Type: "rpm",
SourceURL: "https://old.example.com",
})
newURL := "https://new.example.com"
updated, err := svc.Update(ctx, repo.ID, core.UpdateRepoInput{SourceURL: &newURL})
if err != nil {
t.Fatalf("Update: %v", err)
}
if updated.SourceURL != newURL {
t.Errorf("SourceURL: got %q, want %q", updated.SourceURL, newURL)
}
}
func TestUpdate_changeSyncMode(t *testing.T) {
svc := newTestRepoService(t)
ctx := context.Background()
repo, _ := svc.Create(ctx, core.CreateRepoInput{
Name: "rpm-repo-2",
Type: "rpm",
SourceURL: "https://example.com",
})
mode := "manual"
updated, err := svc.Update(ctx, repo.ID, core.UpdateRepoInput{SyncMode: &mode})
if err != nil {
t.Fatalf("Update: %v", err)
}
if updated.SyncMode != "manual" {
t.Errorf("SyncMode: got %q, want manual", updated.SyncMode)
}
}
func TestUpdate_aptComponents(t *testing.T) {
svc := newTestRepoService(t)
ctx := context.Background()
repo, _ := svc.Create(ctx, core.CreateRepoInput{
Name: "apt-repo",
Type: "apt",
SourceURL: "https://deb.debian.org/debian",
AptSuite: "bookworm",
})
suite := "bookworm-backports"
updated, err := svc.Update(ctx, repo.ID, core.UpdateRepoInput{
AptSuite: &suite,
AptComponents: []string{"main", "contrib", "non-free"},
})
if err != nil {
t.Fatalf("Update: %v", err)
}
var cfg aptclone.Config
if err := json.Unmarshal([]byte(updated.Config), &cfg); err != nil {
t.Fatalf("Config JSON: %v", err)
}
if cfg.Suite != "bookworm-backports" {
t.Errorf("Suite: got %q, want bookworm-backports", cfg.Suite)
}
if len(cfg.Components) != 3 {
t.Errorf("Components: got %v", cfg.Components)
}
}
func TestUpdate_invalidSyncMode(t *testing.T) {
svc := newTestRepoService(t)
ctx := context.Background()
repo, _ := svc.Create(ctx, core.CreateRepoInput{
Name: "rpm-repo-3",
Type: "rpm",
SourceURL: "https://example.com",
})
bad := "weekly"
_, err := svc.Update(ctx, repo.ID, core.UpdateRepoInput{SyncMode: &bad})
if err == nil {
t.Error("expected error for invalid sync_mode, got nil")
}
}
func TestUpdate_aptFieldsOnRPMRepo(t *testing.T) {
svc := newTestRepoService(t)
ctx := context.Background()
repo, _ := svc.Create(ctx, core.CreateRepoInput{
Name: "rpm-repo-4",
Type: "rpm",
SourceURL: "https://example.com",
})
suite := "bookworm"
_, err := svc.Update(ctx, repo.ID, core.UpdateRepoInput{AptSuite: &suite})
if err == nil {
t.Error("expected error applying apt fields to rpm repo, got nil")
}
}
func TestUpdate_emptySourceURL(t *testing.T) {
svc := newTestRepoService(t)
ctx := context.Background()
repo, _ := svc.Create(ctx, core.CreateRepoInput{
Name: "rpm-repo-5",
Type: "rpm",
SourceURL: "https://example.com",
})
empty := ""
_, err := svc.Update(ctx, repo.ID, core.UpdateRepoInput{SourceURL: &empty})
if err == nil {
t.Error("expected error for empty source_url, got nil")
}
}
func TestUpdate_notFound(t *testing.T) {
svc := newTestRepoService(t)
url := "https://example.com"
_, err := svc.Update(context.Background(), 9999, core.UpdateRepoInput{SourceURL: &url})
if !errors.Is(err, store.ErrNotFound) {
t.Errorf("expected ErrNotFound, got %v", err)
}
}

View file

@ -0,0 +1,417 @@
package core_test
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
aptclone "github.com/syonad/clonepack/internal/clone/apt"
rpmclone "github.com/syonad/clonepack/internal/clone/rpm"
"github.com/syonad/clonepack/internal/store"
)
// --- helpers ---
// buildRPMRepo creates an RPM repo (repomd.xml + upstream primary.xml.gz +
// .primary-source marker) in localDir and writes fake package files on disk.
func buildRPMRepo(t *testing.T, localDir string, pkgs []struct{ name, version, href string }) {
t.Helper()
if err := rpmclone.InitEmptyRepo(localDir); err != nil {
t.Fatalf("InitEmptyRepo: %v", err)
}
// Build upstream primary.xml.gz.
var xmlBuf bytes.Buffer
xmlBuf.WriteString(`<?xml version="1.0" encoding="UTF-8"?>`)
fmt.Fprintf(&xmlBuf, `<metadata xmlns="http://linux.duke.edu/metadata/common" packages="%d">`, len(pkgs))
for _, p := range pkgs {
fmt.Fprintf(&xmlBuf,
`<package type="rpm"><name>%s</name>`+
`<version epoch="0" ver="%s" rel="1"/>`+
`<arch>x86_64</arch>`+
`<checksum type="sha256" pkgid="YES">abc%s</checksum>`+
`<size package="1024" installed="2048" archive="1500"/>`+
`<location href="%s"/>`+
`</package>`,
p.name, p.version, p.name, p.href)
}
xmlBuf.WriteString(`</metadata>`)
var gz bytes.Buffer
gzw := gzip.NewWriter(&gz)
gzw.Write(xmlBuf.Bytes())
gzw.Close()
upstreamPath := filepath.Join(localDir, "repodata", "upstream-primary.xml.gz")
if err := os.WriteFile(upstreamPath, gz.Bytes(), 0o644); err != nil {
t.Fatalf("write upstream primary: %v", err)
}
// .primary-source marker so RegenerateMetadata always filters from upstream.
marker := filepath.Join(localDir, "repodata", rpmclone.PrimarySourceFile)
if err := os.WriteFile(marker, []byte("repodata/upstream-primary.xml.gz"), 0o644); err != nil {
t.Fatalf("write .primary-source: %v", err)
}
// Minimal repomd.xml pointing at the upstream (regen will redirect to local).
type loc struct {
Href string `xml:"href,attr"`
}
type chk struct {
Type string `xml:"type,attr"`
Value string `xml:",chardata"`
}
type entry struct {
Type string `xml:"type,attr"`
Location loc `xml:"location"`
Checksum chk `xml:"checksum"`
}
type repomdDoc struct {
XMLName xml.Name `xml:"repomd"`
Data []entry `xml:"data"`
}
rm := repomdDoc{Data: []entry{{
Type: "primary",
Location: loc{Href: "repodata/upstream-primary.xml.gz"},
Checksum: chk{Type: "sha256", Value: "placeholder"},
}}}
repomdXML, _ := xml.MarshalIndent(rm, "", " ")
if err := os.WriteFile(
filepath.Join(localDir, "repodata", "repomd.xml"),
append([]byte(xml.Header), repomdXML...),
0o644,
); err != nil {
t.Fatalf("write repomd.xml: %v", err)
}
// Create fake package files.
for _, p := range pkgs {
dest := filepath.Join(localDir, filepath.FromSlash(p.href))
os.MkdirAll(filepath.Dir(dest), 0o755)
os.WriteFile(dest, []byte("fake-rpm"), 0o644)
}
// Regenerate so repomd.xml reflects actual checksums.
if err := rpmclone.RegenerateMetadata(localDir); err != nil {
t.Fatalf("RegenerateMetadata: %v", err)
}
}
// buildAPTRepo creates an APT repo with Packages files and fake deb files on disk.
func buildAPTRepo(t *testing.T, localDir string, cfg aptclone.Config, debs []struct{ pkg, version, filename string }) {
t.Helper()
if err := aptclone.InitEmptyRepo(localDir, cfg); err != nil {
t.Fatalf("InitEmptyRepo apt: %v", err)
}
var stanzas []string
for _, d := range debs {
stanzas = append(stanzas, strings.Join([]string{
"Package: " + d.pkg,
"Version: " + d.version,
"Architecture: amd64",
"Filename: " + d.filename,
"SHA256: abc" + d.pkg,
"Size: 1024",
}, "\n"))
}
plain := []byte(strings.Join(stanzas, "\n\n") + "\n")
var gz bytes.Buffer
gzw := gzip.NewWriter(&gz)
gzw.Write(plain)
gzw.Close()
gzData := gz.Bytes()
for _, component := range cfg.Components {
for _, arch := range cfg.Architectures {
binDir := filepath.Join(localDir, "dists", cfg.Suite, component, "binary-"+arch)
os.MkdirAll(binDir, 0o755)
os.WriteFile(filepath.Join(binDir, ".upstream-Packages.gz"), gzData, 0o644)
}
}
for _, d := range debs {
dest := filepath.Join(localDir, filepath.FromSlash(d.filename))
os.MkdirAll(filepath.Dir(dest), 0o755)
os.WriteFile(dest, []byte("fake-deb"), 0o644)
}
if err := aptclone.RegenerateMetadata(localDir, cfg); err != nil {
t.Fatalf("RegenerateMetadata apt: %v", err)
}
}
// --- TakeSnapshot RPM ---
func TestTakeSnapshot_RPM_capturesPackages(t *testing.T) {
env := newTestEnv(t)
ctx := context.Background()
repoID, _ := env.repoStore.CreateRepo(ctx, &store.Repo{
Name: "rpm-repo", Type: "rpm", SourceURL: "https://example.com", SyncMode: "manual",
})
localDir := filepath.Join(env.dataDir, "repos", "1", "rpm")
buildRPMRepo(t, localDir, []struct{ name, version, href string }{
{"curl", "7.88", "Packages/curl-7.88-1.x86_64.rpm"},
{"wget", "1.21", "Packages/wget-1.21-1.x86_64.rpm"},
})
snapID, err := env.snapshotSvc.TakeSnapshot(ctx, repoID, "v1")
if err != nil {
t.Fatalf("TakeSnapshot: %v", err)
}
pkgs, _ := env.snapshotStore.GetSnapshotPackages(ctx, snapID)
if len(pkgs) != 2 {
t.Fatalf("expected 2 packages, got %d", len(pkgs))
}
names := map[string]bool{}
for _, p := range pkgs {
names[p.Name] = true
}
if !names["curl"] || !names["wget"] {
t.Errorf("unexpected package names: %v", names)
}
}
func TestTakeSnapshot_RPM_emptyRepo(t *testing.T) {
env := newTestEnv(t)
ctx := context.Background()
repoID, _ := env.repoStore.CreateRepo(ctx, &store.Repo{
Name: "rpm-empty", Type: "rpm", SourceURL: "https://example.com", SyncMode: "manual",
})
localDir := filepath.Join(env.dataDir, "repos", "1", "rpm")
buildRPMRepo(t, localDir, nil)
snapID, err := env.snapshotSvc.TakeSnapshot(ctx, repoID, "empty")
if err != nil {
t.Fatalf("TakeSnapshot: %v", err)
}
pkgs, _ := env.snapshotStore.GetSnapshotPackages(ctx, snapID)
if len(pkgs) != 0 {
t.Errorf("expected 0 packages, got %d", len(pkgs))
}
}
func TestTakeSnapshot_repoNotFound(t *testing.T) {
env := newTestEnv(t)
_, err := env.snapshotSvc.TakeSnapshot(context.Background(), 9999, "v1")
if err == nil {
t.Error("expected error for unknown repo")
}
}
// --- TakeSnapshot APT ---
func TestTakeSnapshot_APT_capturesPackages(t *testing.T) {
env := newTestEnv(t)
ctx := context.Background()
cfg := aptclone.Config{Suite: "bookworm", Components: []string{"main"}, Architectures: []string{"amd64"}}
cfgJSON, _ := json.Marshal(cfg)
repoID, _ := env.repoStore.CreateRepo(ctx, &store.Repo{
Name: "apt-repo", Type: "apt", SourceURL: "https://deb.debian.org/debian",
SyncMode: "manual", Config: string(cfgJSON),
})
localDir := filepath.Join(env.dataDir, "repos", "1", "apt")
buildAPTRepo(t, localDir, cfg, []struct{ pkg, version, filename string }{
{"curl", "7.88.1-10", "pool/main/c/curl/curl_7.88.1-10_amd64.deb"},
{"vim", "2:9.0.1378-2", "pool/main/v/vim/vim_9.0.1378-2_amd64.deb"},
})
snapID, err := env.snapshotSvc.TakeSnapshot(ctx, repoID, "apt-v1")
if err != nil {
t.Fatalf("TakeSnapshot APT: %v", err)
}
pkgs, _ := env.snapshotStore.GetSnapshotPackages(ctx, snapID)
if len(pkgs) != 2 {
t.Fatalf("expected 2 packages, got %d", len(pkgs))
}
}
func TestTakeSnapshot_APT_deduplicatesAcrossComponents(t *testing.T) {
env := newTestEnv(t)
ctx := context.Background()
cfg := aptclone.Config{
Suite: "bookworm", Components: []string{"main", "contrib"}, Architectures: []string{"amd64"},
}
cfgJSON, _ := json.Marshal(cfg)
repoID, _ := env.repoStore.CreateRepo(ctx, &store.Repo{
Name: "apt-multi", Type: "apt", SourceURL: "https://deb.debian.org/debian",
SyncMode: "manual", Config: string(cfgJSON),
})
localDir := filepath.Join(env.dataDir, "repos", "1", "apt")
aptclone.InitEmptyRepo(localDir, cfg)
// Same .deb appears in both components.
stanza := strings.Join([]string{
"Package: shared-pkg",
"Version: 1.0",
"Architecture: amd64",
"Filename: pool/main/s/shared-pkg/shared-pkg_1.0_amd64.deb",
"SHA256: abcdef",
"Size: 1024",
}, "\n") + "\n"
var gz bytes.Buffer
gzw := gzip.NewWriter(&gz)
gzw.Write([]byte(stanza))
gzw.Close()
gzData := gz.Bytes()
for _, component := range cfg.Components {
binDir := filepath.Join(localDir, "dists", cfg.Suite, component, "binary-amd64")
os.MkdirAll(binDir, 0o755)
os.WriteFile(filepath.Join(binDir, ".upstream-Packages.gz"), gzData, 0o644)
}
dest := filepath.Join(localDir, "pool/main/s/shared-pkg/shared-pkg_1.0_amd64.deb")
os.MkdirAll(filepath.Dir(dest), 0o755)
os.WriteFile(dest, []byte("fake-deb"), 0o644)
aptclone.RegenerateMetadata(localDir, cfg)
snapID, err := env.snapshotSvc.TakeSnapshot(ctx, repoID, "dedup-test")
if err != nil {
t.Fatalf("TakeSnapshot: %v", err)
}
pkgs, _ := env.snapshotStore.GetSnapshotPackages(ctx, snapID)
if len(pkgs) != 1 {
t.Errorf("expected 1 deduplicated package, got %d", len(pkgs))
}
}
// --- Diff ---
func TestDiff_addsAndRemoves(t *testing.T) {
env := newTestEnv(t)
ctx := context.Background()
repoID, _ := env.repoStore.CreateRepo(ctx, &store.Repo{
Name: "rpm-repo", Type: "rpm", SourceURL: "https://example.com", SyncMode: "manual",
})
snap1ID, _ := env.snapshotStore.CreateSnapshot(ctx, repoID, "v1")
env.snapshotStore.AddSnapshotPackages(ctx, snap1ID, []store.SnapshotPackage{
{Name: "curl", Version: "7.88-1", Arch: "x86_64", Location: "Packages/curl.rpm", ChecksumType: "sha256"},
{Name: "wget", Version: "1.21-1", Arch: "x86_64", Location: "Packages/wget.rpm", ChecksumType: "sha256"},
})
snap2ID, _ := env.snapshotStore.CreateSnapshot(ctx, repoID, "v2")
env.snapshotStore.AddSnapshotPackages(ctx, snap2ID, []store.SnapshotPackage{
{Name: "curl", Version: "7.88-1", Arch: "x86_64", Location: "Packages/curl.rpm", ChecksumType: "sha256"},
{Name: "vim", Version: "9.0-1", Arch: "x86_64", Location: "Packages/vim.rpm", ChecksumType: "sha256"},
})
diff, err := env.snapshotSvc.Diff(ctx, repoID, snap1ID, snap2ID)
if err != nil {
t.Fatalf("Diff: %v", err)
}
if diff.Unchanged != 1 {
t.Errorf("Unchanged: got %d, want 1", diff.Unchanged)
}
if len(diff.Added) != 1 || diff.Added[0].Name != "vim" {
t.Errorf("Added: got %v, want [vim]", diff.Added)
}
if len(diff.Removed) != 1 || diff.Removed[0].Name != "wget" {
t.Errorf("Removed: got %v, want [wget]", diff.Removed)
}
}
func TestDiff_wrongRepo(t *testing.T) {
env := newTestEnv(t)
ctx := context.Background()
id1, _ := env.repoStore.CreateRepo(ctx, &store.Repo{Name: "r1", Type: "rpm", SourceURL: "https://a.com", SyncMode: "manual"})
id2, _ := env.repoStore.CreateRepo(ctx, &store.Repo{Name: "r2", Type: "rpm", SourceURL: "https://b.com", SyncMode: "manual"})
snap1ID, _ := env.snapshotStore.CreateSnapshot(ctx, id1, "s1")
snap2ID, _ := env.snapshotStore.CreateSnapshot(ctx, id2, "s2")
_, err := env.snapshotSvc.Diff(ctx, id1, snap1ID, snap2ID)
if err == nil {
t.Error("expected error when snapshots belong to different repos")
}
}
// --- Rollback ---
func TestRollback_removesExtraFiles(t *testing.T) {
env := newTestEnv(t)
ctx := context.Background()
repoID, _ := env.repoStore.CreateRepo(ctx, &store.Repo{
Name: "rpm-repo", Type: "rpm", SourceURL: "https://example.com", SyncMode: "manual",
})
localDir := filepath.Join(env.dataDir, "repos", "1", "rpm")
buildRPMRepo(t, localDir, []struct{ name, version, href string }{
{"curl", "7.88", "Packages/curl-7.88-1.x86_64.rpm"},
{"wget", "1.21", "Packages/wget-1.21-1.x86_64.rpm"},
})
// Snapshot records only curl.
snapID, _ := env.snapshotStore.CreateSnapshot(ctx, repoID, "curl-only")
env.snapshotStore.AddSnapshotPackages(ctx, snapID, []store.SnapshotPackage{
{Name: "curl", Version: "7.88-1", Arch: "x86_64",
Location: "Packages/curl-7.88-1.x86_64.rpm", ChecksumType: "sha256"},
})
curlPath := filepath.Join(localDir, "Packages", "curl-7.88-1.x86_64.rpm")
wgetPath := filepath.Join(localDir, "Packages", "wget-1.21-1.x86_64.rpm")
if err := env.snapshotSvc.Rollback(ctx, repoID, snapID); err != nil {
t.Fatalf("Rollback: %v", err)
}
if _, err := os.Stat(wgetPath); err == nil {
t.Error("wget should have been removed by rollback")
}
if _, err := os.Stat(curlPath); err != nil {
t.Errorf("curl should still be present: %v", err)
}
}
func TestRollback_redownloadsMissingFiles(t *testing.T) {
const fileContent = "fake-rpm-bytes"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fileContent))
}))
defer srv.Close()
env := newTestEnv(t)
ctx := context.Background()
repoID, _ := env.repoStore.CreateRepo(ctx, &store.Repo{
Name: "rpm-repo", Type: "rpm", SourceURL: srv.URL, SyncMode: "manual",
})
localDir := filepath.Join(env.dataDir, "repos", "1", "rpm")
buildRPMRepo(t, localDir, []struct{ name, version, href string }{
{"curl", "7.88", "Packages/curl-7.88-1.x86_64.rpm"},
})
snapID, _ := env.snapshotStore.CreateSnapshot(ctx, repoID, "v1")
env.snapshotStore.AddSnapshotPackages(ctx, snapID, []store.SnapshotPackage{
{Name: "curl", Version: "7.88-1", Arch: "x86_64",
Location: "Packages/curl-7.88-1.x86_64.rpm", ChecksumType: "sha256"},
})
curlPath := filepath.Join(localDir, "Packages", "curl-7.88-1.x86_64.rpm")
os.Remove(curlPath)
if err := env.snapshotSvc.Rollback(ctx, repoID, snapID); err != nil {
t.Fatalf("Rollback: %v", err)
}
if _, err := os.Stat(curlPath); err != nil {
t.Errorf("curl should have been re-downloaded: %v", err)
}
}

268
internal/core/sync_test.go Normal file
View file

@ -0,0 +1,268 @@
package core_test
import (
"context"
"database/sql"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/syonad/clonepack/config"
"github.com/syonad/clonepack/internal/core"
"github.com/syonad/clonepack/internal/store"
)
// testEnv regroupe tous les stores + le SyncService câblé sur SQLite in-memory.
type testEnv struct {
db *sql.DB
repoStore *store.SQLiteRepoStore
pendingStore *store.SQLitePendingPackageStore
blockedStore *store.SQLiteBlockedPackageStore
snapshotStore *store.SQLiteSnapshotStore
cloneJobStore *store.SQLiteCloneJobStore
syncSvc *core.SyncService
snapshotSvc *core.SnapshotService
dataDir string
}
func newTestEnv(t *testing.T) *testEnv {
t.Helper()
db, err := store.Open(config.DBConfig{Path: ":memory:"})
if err != nil {
t.Fatalf("open db: %v", err)
}
t.Cleanup(func() { db.Close() })
dataDir := t.TempDir()
repoStore := store.NewRepoStore(db)
pendingStore := store.NewPendingPackageStore(db)
blockedStore := store.NewBlockedPackageStore(db)
snapshotStore := store.NewSnapshotStore(db)
cloneJobStore := store.NewCloneJobStore(db)
cloneSvc := core.NewCloneService(repoStore, cloneJobStore, dataDir)
snapshotSvc := core.NewSnapshotService(snapshotStore, repoStore, dataDir)
syncSvc := core.NewSyncService(repoStore, pendingStore, blockedStore, cloneSvc, snapshotSvc, dataDir)
return &testEnv{
db: db,
repoStore: repoStore,
pendingStore: pendingStore,
blockedStore: blockedStore,
snapshotStore: snapshotStore,
cloneJobStore: cloneJobStore,
syncSvc: syncSvc,
snapshotSvc: snapshotSvc,
dataDir: dataDir,
}
}
func (e *testEnv) createRepo(t *testing.T, name, repoType string) int64 {
t.Helper()
id, err := e.repoStore.CreateRepo(context.Background(), &store.Repo{
Name: name, Type: repoType, SourceURL: "https://example.com", SyncMode: "manual",
})
if err != nil {
t.Fatalf("createRepo %q: %v", name, err)
}
return id
}
func (e *testEnv) seedPending(t *testing.T, repoID int64, names ...string) []store.PendingPackage {
t.Helper()
pkgs := make([]store.PendingPackage, len(names))
for i, name := range names {
pkgs[i] = store.PendingPackage{
RepoID: repoID, Name: name, Version: "1.0", Arch: "amd64",
Location: "Packages/" + name + ".rpm", Checksum: "abc", ChecksumType: "sha256", Size: 100,
}
}
if err := e.pendingStore.UpsertPending(context.Background(), pkgs); err != nil {
t.Fatalf("seedPending: %v", err)
}
listed, _ := e.pendingStore.ListPending(context.Background(), repoID)
return listed
}
// --- ListPending ---
func TestSyncListPending(t *testing.T) {
env := newTestEnv(t)
repoID := env.createRepo(t, "rpm-repo", "rpm")
env.seedPending(t, repoID, "curl", "wget")
pkgs, err := env.syncSvc.ListPending(context.Background(), repoID)
if err != nil {
t.Fatalf("ListPending: %v", err)
}
if len(pkgs) != 2 {
t.Errorf("expected 2 pending, got %d", len(pkgs))
}
}
func TestSyncListPending_repoNotFound(t *testing.T) {
env := newTestEnv(t)
_, err := env.syncSvc.ListPending(context.Background(), 9999)
if err == nil {
t.Error("expected error for unknown repo")
}
}
// --- RejectPending ---
func TestRejectPending(t *testing.T) {
env := newTestEnv(t)
repoID := env.createRepo(t, "rpm-repo", "rpm")
listed := env.seedPending(t, repoID, "curl", "wget", "vim")
ids := []int64{listed[0].ID, listed[1].ID}
if err := env.syncSvc.RejectPending(context.Background(), repoID, ids); err != nil {
t.Fatalf("RejectPending: %v", err)
}
remaining, _ := env.pendingStore.ListPending(context.Background(), repoID)
if len(remaining) != 1 {
t.Fatalf("expected 1 remaining, got %d", len(remaining))
}
if remaining[0].Name != "vim" {
t.Errorf("expected vim remaining, got %q", remaining[0].Name)
}
}
// --- BlockPackages ---
func TestBlockPackages_removesFromPendingAndAddsToBlocked(t *testing.T) {
env := newTestEnv(t)
repoID := env.createRepo(t, "rpm-repo", "rpm")
listed := env.seedPending(t, repoID, "curl", "wget", "vim")
// Block curl and wget (by pending ID).
ids := []int64{listed[0].ID, listed[1].ID}
if err := env.syncSvc.BlockPackages(context.Background(), repoID, ids); err != nil {
t.Fatalf("BlockPackages: %v", err)
}
// Pending should only have vim left.
pending, _ := env.pendingStore.ListPending(context.Background(), repoID)
if len(pending) != 1 || pending[0].Name != "vim" {
t.Errorf("pending after block: expected [vim], got %v", pending)
}
// Blocked should have curl and wget.
blocked, _ := env.blockedStore.ListBlocked(context.Background(), repoID)
if len(blocked) != 2 {
t.Fatalf("blocked: expected 2, got %d", len(blocked))
}
names := map[string]bool{blocked[0].Name: true, blocked[1].Name: true}
if !names["curl"] || !names["wget"] {
t.Errorf("blocked names: got %v, want curl and wget", names)
}
}
// --- UnblockPackages ---
func TestUnblockPackages(t *testing.T) {
env := newTestEnv(t)
repoID := env.createRepo(t, "rpm-repo", "rpm")
listed := env.seedPending(t, repoID, "curl", "wget")
env.syncSvc.BlockPackages(context.Background(), repoID, []int64{listed[0].ID, listed[1].ID})
blocked, _ := env.blockedStore.ListBlocked(context.Background(), repoID)
if len(blocked) != 2 {
t.Fatalf("setup: expected 2 blocked, got %d", len(blocked))
}
if err := env.syncSvc.UnblockPackages(context.Background(), repoID, []int64{blocked[0].ID}); err != nil {
t.Fatalf("UnblockPackages: %v", err)
}
remaining, _ := env.blockedStore.ListBlocked(context.Background(), repoID)
if len(remaining) != 1 {
t.Fatalf("expected 1 blocked remaining, got %d", len(remaining))
}
}
// --- ApprovePending ---
func TestApprovePending_downloadsAndRemovesFromPending(t *testing.T) {
// Serve a fake package file.
const fileContent = "fake-rpm-content"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fileContent))
}))
defer srv.Close()
env := newTestEnv(t)
ctx := context.Background()
// Repo with source URL pointing to our test server.
repoID, _ := env.repoStore.CreateRepo(ctx, &store.Repo{
Name: "rpm-repo", Type: "rpm", SourceURL: srv.URL, SyncMode: "manual",
})
// Create the local repo dir so DownloadAndVerify can write.
localDir := filepath.Join(env.dataDir, "repos", "1", "rpm")
os.MkdirAll(localDir, 0o755)
// Seed one pending package. Checksum left empty so verify is skipped.
pkg := store.PendingPackage{
RepoID: repoID, Name: "curl", Version: "7.88", Arch: "amd64",
Location: "Packages/curl.rpm", Checksum: "", ChecksumType: "sha256", Size: int64(len(fileContent)),
}
env.pendingStore.UpsertPending(ctx, []store.PendingPackage{pkg})
listed, _ := env.pendingStore.ListPending(ctx, repoID)
if err := env.syncSvc.ApprovePending(ctx, repoID, []int64{listed[0].ID}); err != nil {
t.Fatalf("ApprovePending: %v", err)
}
// Package must be removed from pending.
remaining, _ := env.pendingStore.ListPending(ctx, repoID)
if len(remaining) != 0 {
t.Errorf("expected empty pending after approve, got %d", len(remaining))
}
// File must exist on disk.
dest := filepath.Join(localDir, "Packages", "curl.rpm")
if _, err := os.Stat(dest); err != nil {
t.Errorf("expected file on disk at %s: %v", dest, err)
}
}
func TestApprovePending_continuesOnDownloadError(t *testing.T) {
// Server returns 404 for everything.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}))
defer srv.Close()
env := newTestEnv(t)
ctx := context.Background()
repoID, _ := env.repoStore.CreateRepo(ctx, &store.Repo{
Name: "rpm-repo", Type: "rpm", SourceURL: srv.URL, SyncMode: "manual",
})
env.pendingStore.UpsertPending(ctx, []store.PendingPackage{
{RepoID: repoID, Name: "curl", Version: "1.0", Arch: "amd64", Location: "pkg/curl.rpm", ChecksumType: "sha256"},
{RepoID: repoID, Name: "wget", Version: "1.0", Arch: "amd64", Location: "pkg/wget.rpm", ChecksumType: "sha256"},
})
listed, _ := env.pendingStore.ListPending(ctx, repoID)
ids := []int64{listed[0].ID, listed[1].ID}
// Should return errors but not panic.
err := env.syncSvc.ApprovePending(ctx, repoID, ids)
if err == nil {
t.Error("expected errors for 404 downloads, got nil")
}
// Nothing approved — pending should remain.
remaining, _ := env.pendingStore.ListPending(ctx, repoID)
if len(remaining) != 2 {
t.Errorf("expected 2 still pending, got %d", len(remaining))
}
}