add test for the wall app
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
c4776d81dd
commit
9e287e4e6a
15 changed files with 3022 additions and 1 deletions
354
internal/api/handler_test.go
Normal file
354
internal/api/handler_test.go
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/syonad/clonepack/config"
|
||||
"github.com/syonad/clonepack/internal/api"
|
||||
"github.com/syonad/clonepack/internal/core"
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
// testServer wires up a full in-memory stack and returns an httptest.Server.
|
||||
func newTestServer(t *testing.T) (*httptest.Server, *sql.DB) {
|
||||
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)
|
||||
jobStore := store.NewCloneJobStore(db)
|
||||
pendingStore := store.NewPendingPackageStore(db)
|
||||
blockedStore := store.NewBlockedPackageStore(db)
|
||||
snapshotStore := store.NewSnapshotStore(db)
|
||||
|
||||
repoSvc := core.NewRepoService(repoStore, dataDir)
|
||||
cloneSvc := core.NewCloneService(repoStore, jobStore, dataDir)
|
||||
snapshotSvc := core.NewSnapshotService(snapshotStore, repoStore, dataDir)
|
||||
syncSvc := core.NewSyncService(repoStore, pendingStore, blockedStore, cloneSvc, snapshotSvc, dataDir)
|
||||
|
||||
router := api.NewRouter(
|
||||
api.NewRepoHandler(repoSvc),
|
||||
api.NewCloneHandler(cloneSvc),
|
||||
api.NewSyncHandler(syncSvc),
|
||||
api.NewSnapshotHandler(snapshotSvc),
|
||||
api.NewProxyHandler(repoSvc, dataDir),
|
||||
)
|
||||
|
||||
srv := httptest.NewServer(router)
|
||||
t.Cleanup(srv.Close)
|
||||
return srv, db
|
||||
}
|
||||
|
||||
func do(t *testing.T, method, url string, body any) *http.Response {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
if body != nil {
|
||||
json.NewEncoder(&buf).Encode(body)
|
||||
}
|
||||
req, err := http.NewRequest(method, url, &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", method, url, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func decodeJSON(t *testing.T, resp *http.Response, v any) {
|
||||
t.Helper()
|
||||
defer resp.Body.Close()
|
||||
if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Health ---
|
||||
|
||||
func TestHealth(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
resp := do(t, http.MethodGet, srv.URL+"/health", nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("health: got %d, want 200", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Create repo ---
|
||||
|
||||
func TestCreateRepo_rpm(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
|
||||
payload := map[string]any{
|
||||
"name": "rocky9",
|
||||
"type": "rpm",
|
||||
"source_url": "https://download.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os",
|
||||
}
|
||||
resp := do(t, http.MethodPost, srv.URL+"/api/v1/repos", payload)
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("create rpm: got %d, want 201", resp.StatusCode)
|
||||
}
|
||||
|
||||
var repo api.RepoResponse
|
||||
decodeJSON(t, resp, &repo)
|
||||
if repo.ID <= 0 {
|
||||
t.Errorf("expected positive ID, got %d", repo.ID)
|
||||
}
|
||||
if repo.Name != "rocky9" {
|
||||
t.Errorf("Name: got %q, want rocky9", repo.Name)
|
||||
}
|
||||
if repo.SyncMode != "auto" {
|
||||
t.Errorf("SyncMode default: got %q, want auto", repo.SyncMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRepo_apt(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
|
||||
payload := map[string]any{
|
||||
"name": "debian-bookworm",
|
||||
"type": "apt",
|
||||
"source_url": "https://deb.debian.org/debian",
|
||||
"apt_suite": "bookworm",
|
||||
"apt_components": []string{"main", "contrib"},
|
||||
"apt_architectures": []string{"amd64"},
|
||||
}
|
||||
resp := do(t, http.MethodPost, srv.URL+"/api/v1/repos", payload)
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("create apt: got %d, want 201", resp.StatusCode)
|
||||
}
|
||||
|
||||
var repo api.RepoResponse
|
||||
decodeJSON(t, resp, &repo)
|
||||
if repo.AptSuite != "bookworm" {
|
||||
t.Errorf("AptSuite: got %q, want bookworm", repo.AptSuite)
|
||||
}
|
||||
if len(repo.AptComponents) != 2 {
|
||||
t.Errorf("AptComponents: got %v", repo.AptComponents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRepo_invalidType(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
resp := do(t, http.MethodPost, srv.URL+"/api/v1/repos", map[string]any{
|
||||
"name": "bad", "type": "maven", "source_url": "https://x.com",
|
||||
})
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Errorf("invalid type: got %d, want 422", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func TestCreateRepo_aptMissingSuite(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
resp := do(t, http.MethodPost, srv.URL+"/api/v1/repos", map[string]any{
|
||||
"name": "apt-no-suite", "type": "apt", "source_url": "https://deb.debian.org/debian",
|
||||
})
|
||||
if resp.StatusCode != http.StatusUnprocessableEntity {
|
||||
t.Errorf("missing suite: got %d, want 422", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func TestCreateRepo_invalidJSON(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/api/v1/repos", bytes.NewBufferString("not-json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, _ := http.DefaultClient.Do(req)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("bad json: got %d, want 400", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// --- List repos ---
|
||||
|
||||
func TestListRepos_empty(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
resp := do(t, http.MethodGet, srv.URL+"/api/v1/repos", nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("list: got %d, want 200", resp.StatusCode)
|
||||
}
|
||||
var body api.ListReposResponse
|
||||
decodeJSON(t, resp, &body)
|
||||
if body.Total != 0 {
|
||||
t.Errorf("expected 0 repos, got %d", body.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRepos_afterCreate(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
|
||||
do(t, http.MethodPost, srv.URL+"/api/v1/repos", map[string]any{
|
||||
"name": "repo-a", "type": "rpm", "source_url": "https://a.com",
|
||||
}).Body.Close()
|
||||
do(t, http.MethodPost, srv.URL+"/api/v1/repos", map[string]any{
|
||||
"name": "repo-b", "type": "rpm", "source_url": "https://b.com",
|
||||
}).Body.Close()
|
||||
|
||||
resp := do(t, http.MethodGet, srv.URL+"/api/v1/repos", nil)
|
||||
var body api.ListReposResponse
|
||||
decodeJSON(t, resp, &body)
|
||||
if body.Total != 2 {
|
||||
t.Errorf("expected 2 repos, got %d", body.Total)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Get repo ---
|
||||
|
||||
func TestGetRepo(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
|
||||
createResp := do(t, http.MethodPost, srv.URL+"/api/v1/repos", map[string]any{
|
||||
"name": "rocky9", "type": "rpm", "source_url": "https://example.com",
|
||||
})
|
||||
var created api.RepoResponse
|
||||
decodeJSON(t, createResp, &created)
|
||||
|
||||
resp := do(t, http.MethodGet, srv.URL+"/api/v1/repos/1", nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("get: got %d, want 200", resp.StatusCode)
|
||||
}
|
||||
var repo api.RepoResponse
|
||||
decodeJSON(t, resp, &repo)
|
||||
if repo.ID != created.ID {
|
||||
t.Errorf("ID: got %d, want %d", repo.ID, created.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepo_notFound(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
resp := do(t, http.MethodGet, srv.URL+"/api/v1/repos/999", nil)
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("not found: got %d, want 404", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// --- Update repo ---
|
||||
|
||||
func TestUpdateRepo_sourceURL(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
|
||||
do(t, http.MethodPost, srv.URL+"/api/v1/repos", map[string]any{
|
||||
"name": "rocky9", "type": "rpm", "source_url": "https://old.example.com",
|
||||
}).Body.Close()
|
||||
|
||||
resp := do(t, http.MethodPatch, srv.URL+"/api/v1/repos/1", map[string]any{
|
||||
"source_url": "https://new.example.com",
|
||||
})
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("update: got %d, want 200", resp.StatusCode)
|
||||
}
|
||||
var repo api.RepoResponse
|
||||
decodeJSON(t, resp, &repo)
|
||||
if repo.SourceURL != "https://new.example.com" {
|
||||
t.Errorf("SourceURL: got %q", repo.SourceURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRepo_aptConfig(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
|
||||
do(t, http.MethodPost, srv.URL+"/api/v1/repos", map[string]any{
|
||||
"name": "debian", "type": "apt", "source_url": "https://deb.debian.org/debian",
|
||||
"apt_suite": "bookworm",
|
||||
}).Body.Close()
|
||||
|
||||
resp := do(t, http.MethodPatch, srv.URL+"/api/v1/repos/1", map[string]any{
|
||||
"apt_components": []string{"main", "contrib", "non-free"},
|
||||
})
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("update apt: got %d, want 200", resp.StatusCode)
|
||||
}
|
||||
var repo api.RepoResponse
|
||||
decodeJSON(t, resp, &repo)
|
||||
if len(repo.AptComponents) != 3 {
|
||||
t.Errorf("AptComponents: got %v", repo.AptComponents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRepo_notFound(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
resp := do(t, http.MethodPatch, srv.URL+"/api/v1/repos/999", map[string]any{
|
||||
"source_url": "https://x.com",
|
||||
})
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("not found: got %d, want 404", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// --- Delete repo ---
|
||||
|
||||
func TestDeleteRepo(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
|
||||
do(t, http.MethodPost, srv.URL+"/api/v1/repos", map[string]any{
|
||||
"name": "to-delete", "type": "rpm", "source_url": "https://example.com",
|
||||
}).Body.Close()
|
||||
|
||||
resp := do(t, http.MethodDelete, srv.URL+"/api/v1/repos/1", nil)
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("delete: got %d, want 204", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
resp = do(t, http.MethodGet, srv.URL+"/api/v1/repos/1", nil)
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("after delete: got %d, want 404", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func TestDeleteRepo_notFound(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
resp := do(t, http.MethodDelete, srv.URL+"/api/v1/repos/999", nil)
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("not found: got %d, want 404", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// --- Sync ---
|
||||
|
||||
func TestSyncTrigger_notFound(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
resp := do(t, http.MethodPost, srv.URL+"/api/v1/repos/999/sync/trigger", nil)
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("sync trigger not found: got %d, want 404", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func TestSyncListPending_empty(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
do(t, http.MethodPost, srv.URL+"/api/v1/repos", map[string]any{
|
||||
"name": "rpm-repo", "type": "rpm", "source_url": "https://example.com",
|
||||
}).Body.Close()
|
||||
|
||||
resp := do(t, http.MethodGet, srv.URL+"/api/v1/repos/1/sync/pending", nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("sync pending: got %d, want 200", resp.StatusCode)
|
||||
}
|
||||
var body struct {
|
||||
Total int `json:"total"`
|
||||
}
|
||||
decodeJSON(t, resp, &body)
|
||||
if body.Total != 0 {
|
||||
t.Errorf("expected empty pending list, got total=%d", body.Total)
|
||||
}
|
||||
}
|
||||
278
internal/clone/apt/metadata_gen_test.go
Normal file
278
internal/clone/apt/metadata_gen_test.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
package apt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testCfg = Config{
|
||||
Suite: "bookworm",
|
||||
Components: []string{"main"},
|
||||
Architectures: []string{"amd64"},
|
||||
}
|
||||
|
||||
// buildPackagesGz creates a gzipped Packages file from a list of stanzas.
|
||||
func buildPackagesGz(t *testing.T, stanzas []string) []byte {
|
||||
t.Helper()
|
||||
plain := []byte(strings.Join(stanzas, "\n\n") + "\n")
|
||||
|
||||
var buf bytes.Buffer
|
||||
gzw := gzip.NewWriter(&buf)
|
||||
gzw.Write(plain)
|
||||
gzw.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func makeStanza(pkg, version, filename, sha256 string, size int) string {
|
||||
return strings.Join([]string{
|
||||
"Package: " + pkg,
|
||||
"Version: " + version,
|
||||
"Architecture: amd64",
|
||||
"Filename: " + filename,
|
||||
"SHA256: " + sha256,
|
||||
"Size: " + itoa(size),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
buf := make([]byte, 0, 10)
|
||||
for n > 0 {
|
||||
buf = append([]byte{byte('0' + n%10)}, buf...)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// setupAptRepo creates the directory structure and writes the upstream
|
||||
// Packages.gz for component/arch. It creates the given deb files on disk.
|
||||
func setupAptRepo(t *testing.T, stanzas []string, presentOnDisk []string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
|
||||
binDir := filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64")
|
||||
os.MkdirAll(binDir, 0o755)
|
||||
os.MkdirAll(filepath.Join(dir, "pool"), 0o755)
|
||||
|
||||
// Write upstream Packages.gz.
|
||||
gzData := buildPackagesGz(t, stanzas)
|
||||
if err := os.WriteFile(filepath.Join(binDir, upstreamPackagesGz), gzData, 0o644); err != nil {
|
||||
t.Fatalf("write upstream Packages.gz: %v", err)
|
||||
}
|
||||
|
||||
// Create selected deb files on disk.
|
||||
for _, filename := range presentOnDisk {
|
||||
dest := filepath.Join(dir, filepath.FromSlash(filename))
|
||||
os.MkdirAll(filepath.Dir(dest), 0o755)
|
||||
if err := os.WriteFile(dest, []byte("fake-deb"), 0o644); err != nil {
|
||||
t.Fatalf("write deb %s: %v", filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
// readLocalPackages reads and parses the local Packages file (plain text).
|
||||
func readLocalPackages(t *testing.T, dir string) []DebPackage {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64", "Packages")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read Packages: %v", err)
|
||||
}
|
||||
pkgs, err := ParsePackages(data)
|
||||
if err != nil {
|
||||
t.Fatalf("parse Packages: %v", err)
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// --- InitEmptyRepo ---
|
||||
|
||||
func TestInitEmptyRepo_createsStructure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := InitEmptyRepo(dir, testCfg); err != nil {
|
||||
t.Fatalf("InitEmptyRepo: %v", err)
|
||||
}
|
||||
|
||||
for _, path := range []string{
|
||||
filepath.Join(dir, "pool"),
|
||||
filepath.Join(dir, "dists", testCfg.Suite),
|
||||
filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64"),
|
||||
filepath.Join(dir, "dists", testCfg.Suite, "Release"),
|
||||
} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Errorf("expected %s to exist: %v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitEmptyRepo_releaseIsParseable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
InitEmptyRepo(dir, testCfg)
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "dists", testCfg.Suite, "Release"))
|
||||
rf, err := ParseRelease(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseRelease on generated file: %v", err)
|
||||
}
|
||||
if rf.Suite != testCfg.Suite {
|
||||
t.Errorf("Suite: got %q, want %q", rf.Suite, testCfg.Suite)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitEmptyRepo_idempotent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
InitEmptyRepo(dir, testCfg)
|
||||
|
||||
releaseBefore, _ := os.ReadFile(filepath.Join(dir, "dists", testCfg.Suite, "Release"))
|
||||
InitEmptyRepo(dir, testCfg) // second call
|
||||
releaseAfter, _ := os.ReadFile(filepath.Join(dir, "dists", testCfg.Suite, "Release"))
|
||||
|
||||
if !bytes.Equal(releaseBefore, releaseAfter) {
|
||||
t.Error("second InitEmptyRepo modified Release file")
|
||||
}
|
||||
}
|
||||
|
||||
// --- RegenerateMetadata ---
|
||||
|
||||
func TestRegenerateMetadata_keepsOnlyPresentFiles(t *testing.T) {
|
||||
stanzas := []string{
|
||||
makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100),
|
||||
makeStanza("wget", "1.21", "pool/main/w/wget/wget_1.21_amd64.deb", "bbb", 200),
|
||||
makeStanza("vim", "9.0", "pool/main/v/vim/vim_9.0_amd64.deb", "ccc", 300),
|
||||
}
|
||||
// Only curl and vim are present.
|
||||
present := []string{
|
||||
"pool/main/c/curl/curl_7.88_amd64.deb",
|
||||
"pool/main/v/vim/vim_9.0_amd64.deb",
|
||||
}
|
||||
dir := setupAptRepo(t, stanzas, present)
|
||||
|
||||
if err := RegenerateMetadata(dir, testCfg); err != nil {
|
||||
t.Fatalf("RegenerateMetadata: %v", err)
|
||||
}
|
||||
|
||||
pkgs := readLocalPackages(t, dir)
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("expected 2 packages, got %d", len(pkgs))
|
||||
}
|
||||
names := map[string]bool{pkgs[0].Package: true, pkgs[1].Package: true}
|
||||
if !names["curl"] {
|
||||
t.Error("expected curl in output")
|
||||
}
|
||||
if !names["vim"] {
|
||||
t.Error("expected vim in output")
|
||||
}
|
||||
if names["wget"] {
|
||||
t.Error("wget should be filtered out (not on disk)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegenerateMetadata_allAbsent(t *testing.T) {
|
||||
stanzas := []string{
|
||||
makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100),
|
||||
}
|
||||
dir := setupAptRepo(t, stanzas, nil) // nothing on disk
|
||||
|
||||
if err := RegenerateMetadata(dir, testCfg); err != nil {
|
||||
t.Fatalf("RegenerateMetadata: %v", err)
|
||||
}
|
||||
|
||||
pkgs := readLocalPackages(t, dir)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegenerateMetadata_writesRelease(t *testing.T) {
|
||||
stanzas := []string{
|
||||
makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100),
|
||||
}
|
||||
dir := setupAptRepo(t, stanzas, []string{"pool/main/c/curl/curl_7.88_amd64.deb"})
|
||||
|
||||
if err := RegenerateMetadata(dir, testCfg); err != nil {
|
||||
t.Fatalf("RegenerateMetadata: %v", err)
|
||||
}
|
||||
|
||||
releasePath := filepath.Join(dir, "dists", testCfg.Suite, "Release")
|
||||
data, err := os.ReadFile(releasePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Release not written: %v", err)
|
||||
}
|
||||
|
||||
rf, err := ParseRelease(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseRelease: %v", err)
|
||||
}
|
||||
if rf.Suite != testCfg.Suite {
|
||||
t.Errorf("Suite: got %q, want %q", rf.Suite, testCfg.Suite)
|
||||
}
|
||||
// Release must reference both Packages and Packages.gz.
|
||||
paths := map[string]bool{}
|
||||
for _, e := range rf.SHA256 {
|
||||
paths[e.Path] = true
|
||||
}
|
||||
if !paths["main/binary-amd64/Packages"] {
|
||||
t.Error("Release missing main/binary-amd64/Packages entry")
|
||||
}
|
||||
if !paths["main/binary-amd64/Packages.gz"] {
|
||||
t.Error("Release missing main/binary-amd64/Packages.gz entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegenerateMetadata_writesPackagesGz(t *testing.T) {
|
||||
stanzas := []string{
|
||||
makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100),
|
||||
}
|
||||
dir := setupAptRepo(t, stanzas, []string{"pool/main/c/curl/curl_7.88_amd64.deb"})
|
||||
|
||||
RegenerateMetadata(dir, testCfg)
|
||||
|
||||
gzPath := filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64", "Packages.gz")
|
||||
if _, err := os.Stat(gzPath); err != nil {
|
||||
t.Errorf("Packages.gz not written: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegenerateMetadata_noUpstream_skips(t *testing.T) {
|
||||
// No .upstream-Packages.gz — should return nil (skip silently).
|
||||
dir := t.TempDir()
|
||||
os.MkdirAll(filepath.Join(dir, "dists", testCfg.Suite, "main", "binary-amd64"), 0o755)
|
||||
|
||||
if err := RegenerateMetadata(dir, testCfg); err != nil {
|
||||
t.Errorf("expected no error when upstream missing, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegenerateMetadata_alwaysFiltersFromUpstream(t *testing.T) {
|
||||
// Two successive regens must always start from the upstream list.
|
||||
stanzas := []string{
|
||||
makeStanza("curl", "7.88", "pool/main/c/curl/curl_7.88_amd64.deb", "aaa", 100),
|
||||
makeStanza("wget", "1.21", "pool/main/w/wget/wget_1.21_amd64.deb", "bbb", 200),
|
||||
}
|
||||
dir := setupAptRepo(t, stanzas, []string{"pool/main/c/curl/curl_7.88_amd64.deb"})
|
||||
|
||||
// First regen: only curl.
|
||||
RegenerateMetadata(dir, testCfg)
|
||||
if len(readLocalPackages(t, dir)) != 1 {
|
||||
t.Fatal("first regen: expected 1 package")
|
||||
}
|
||||
|
||||
// Add wget, regen again.
|
||||
dest := filepath.Join(dir, "pool/main/w/wget/wget_1.21_amd64.deb")
|
||||
os.MkdirAll(filepath.Dir(dest), 0o755)
|
||||
os.WriteFile(dest, []byte("fake"), 0o644)
|
||||
|
||||
RegenerateMetadata(dir, testCfg)
|
||||
pkgs := readLocalPackages(t, dir)
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("second regen: expected 2 packages, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
138
internal/clone/apt/packages_test.go
Normal file
138
internal/clone/apt/packages_test.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package apt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const sampleStanza = `Package: curl
|
||||
Version: 7.88.1-10+deb12u5
|
||||
Architecture: amd64
|
||||
Filename: pool/main/c/curl/curl_7.88.1-10+deb12u5_amd64.deb
|
||||
SHA256: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
|
||||
Size: 314456`
|
||||
|
||||
const sampleStanza2 = `Package: wget
|
||||
Version: 1.21.3-1+b1
|
||||
Architecture: amd64
|
||||
Filename: pool/main/w/wget/wget_1.21.3-1+b1_amd64.deb
|
||||
SHA256: 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
|
||||
Size: 512000`
|
||||
|
||||
func TestParsePackages_single(t *testing.T) {
|
||||
pkgs, err := ParsePackages([]byte(sampleStanza))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("expected 1 package, got %d", len(pkgs))
|
||||
}
|
||||
p := pkgs[0]
|
||||
if p.Package != "curl" {
|
||||
t.Errorf("Package: got %q, want %q", p.Package, "curl")
|
||||
}
|
||||
if p.Version != "7.88.1-10+deb12u5" {
|
||||
t.Errorf("Version: got %q, want %q", p.Version, "7.88.1-10+deb12u5")
|
||||
}
|
||||
if p.Architecture != "amd64" {
|
||||
t.Errorf("Architecture: got %q, want %q", p.Architecture, "amd64")
|
||||
}
|
||||
if p.Filename != "pool/main/c/curl/curl_7.88.1-10+deb12u5_amd64.deb" {
|
||||
t.Errorf("Filename: got %q", p.Filename)
|
||||
}
|
||||
if p.SHA256 != "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" {
|
||||
t.Errorf("SHA256: got %q", p.SHA256)
|
||||
}
|
||||
if p.Size != 314456 {
|
||||
t.Errorf("Size: got %d, want %d", p.Size, 314456)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePackages_multiple(t *testing.T) {
|
||||
input := sampleStanza + "\n\n" + sampleStanza2
|
||||
pkgs, err := ParsePackages([]byte(input))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(pkgs) != 2 {
|
||||
t.Fatalf("expected 2 packages, got %d", len(pkgs))
|
||||
}
|
||||
if pkgs[0].Package != "curl" {
|
||||
t.Errorf("first package: got %q, want curl", pkgs[0].Package)
|
||||
}
|
||||
if pkgs[1].Package != "wget" {
|
||||
t.Errorf("second package: got %q, want wget", pkgs[1].Package)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePackages_empty(t *testing.T) {
|
||||
pkgs, err := ParsePackages([]byte(""))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePackages_missingPackageField(t *testing.T) {
|
||||
bad := `Version: 1.0
|
||||
Architecture: amd64
|
||||
Filename: pool/main/x/x/x_1.0_amd64.deb
|
||||
SHA256: 0000000000000000000000000000000000000000000000000000000000000000
|
||||
Size: 100`
|
||||
_, err := ParsePackages([]byte(bad))
|
||||
if err == nil {
|
||||
t.Error("expected error for missing Package field, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPackages(t *testing.T) {
|
||||
pkgs, _ := ParsePackages([]byte(sampleStanza + "\n\n" + sampleStanza2))
|
||||
|
||||
// Only curl is "on disk".
|
||||
onDisk := map[string]bool{
|
||||
"pool/main/c/curl/curl_7.88.1-10+deb12u5_amd64.deb": true,
|
||||
}
|
||||
filtered := FilterPackages(pkgs, func(filename string) bool {
|
||||
return onDisk[filename]
|
||||
})
|
||||
|
||||
if len(filtered) != 1 {
|
||||
t.Fatalf("expected 1 filtered package, got %d", len(filtered))
|
||||
}
|
||||
if filtered[0].Package != "curl" {
|
||||
t.Errorf("expected curl, got %q", filtered[0].Package)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEmitRoundTrip(t *testing.T) {
|
||||
input := []byte(sampleStanza + "\n\n" + sampleStanza2 + "\n")
|
||||
pkgs, err := ParsePackages(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
|
||||
emitted := emitPackages(pkgs)
|
||||
|
||||
reparsed, err := ParsePackages(emitted)
|
||||
if err != nil {
|
||||
t.Fatalf("reparse: %v", err)
|
||||
}
|
||||
if len(reparsed) != len(pkgs) {
|
||||
t.Fatalf("round-trip length: got %d, want %d", len(reparsed), len(pkgs))
|
||||
}
|
||||
for i := range pkgs {
|
||||
if reparsed[i].Package != pkgs[i].Package {
|
||||
t.Errorf("[%d] Package: got %q, want %q", i, reparsed[i].Package, pkgs[i].Package)
|
||||
}
|
||||
if reparsed[i].Filename != pkgs[i].Filename {
|
||||
t.Errorf("[%d] Filename: got %q, want %q", i, reparsed[i].Filename, pkgs[i].Filename)
|
||||
}
|
||||
if reparsed[i].SHA256 != pkgs[i].SHA256 {
|
||||
t.Errorf("[%d] SHA256: got %q, want %q", i, reparsed[i].SHA256, pkgs[i].SHA256)
|
||||
}
|
||||
if reparsed[i].Size != pkgs[i].Size {
|
||||
t.Errorf("[%d] Size: got %d, want %d", i, reparsed[i].Size, pkgs[i].Size)
|
||||
}
|
||||
}
|
||||
}
|
||||
126
internal/clone/apt/release_test.go
Normal file
126
internal/clone/apt/release_test.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package apt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const plainRelease = `Origin: Debian
|
||||
Suite: bookworm
|
||||
Codename: bookworm
|
||||
Components: main contrib
|
||||
Architectures: amd64 arm64
|
||||
SHA256:
|
||||
abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 12345 main/binary-amd64/Packages
|
||||
0000111122223333000011112222333300001111222233330000111122223333 67890 main/binary-amd64/Packages.gz
|
||||
`
|
||||
|
||||
const pgpArmoredRelease = `-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
Origin: Debian
|
||||
Suite: bookworm
|
||||
Codename: bookworm
|
||||
Components: main contrib
|
||||
Architectures: amd64
|
||||
SHA256:
|
||||
abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 12345 main/binary-amd64/Packages
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQEzBAABCgAdFiEE2F...fake-signature...
|
||||
-----END PGP SIGNATURE-----
|
||||
`
|
||||
|
||||
func TestParseRelease_plain(t *testing.T) {
|
||||
rf, err := ParseRelease([]byte(plainRelease))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if rf.Suite != "bookworm" {
|
||||
t.Errorf("Suite: got %q, want bookworm", rf.Suite)
|
||||
}
|
||||
if rf.Codename != "bookworm" {
|
||||
t.Errorf("Codename: got %q, want bookworm", rf.Codename)
|
||||
}
|
||||
if rf.Components != "main contrib" {
|
||||
t.Errorf("Components: got %q, want %q", rf.Components, "main contrib")
|
||||
}
|
||||
if rf.Architectures != "amd64 arm64" {
|
||||
t.Errorf("Architectures: got %q, want %q", rf.Architectures, "amd64 arm64")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRelease_sha256entries(t *testing.T) {
|
||||
rf, err := ParseRelease([]byte(plainRelease))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(rf.SHA256) != 2 {
|
||||
t.Fatalf("SHA256 entries: got %d, want 2", len(rf.SHA256))
|
||||
}
|
||||
e := rf.SHA256[0]
|
||||
if e.Hash != "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" {
|
||||
t.Errorf("entry[0] Hash: got %q", e.Hash)
|
||||
}
|
||||
if e.Size != 12345 {
|
||||
t.Errorf("entry[0] Size: got %d, want 12345", e.Size)
|
||||
}
|
||||
if e.Path != "main/binary-amd64/Packages" {
|
||||
t.Errorf("entry[0] Path: got %q", e.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRelease_pgpArmored(t *testing.T) {
|
||||
rf, err := ParseRelease([]byte(pgpArmoredRelease))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if rf.Suite != "bookworm" {
|
||||
t.Errorf("Suite: got %q, want bookworm", rf.Suite)
|
||||
}
|
||||
if len(rf.SHA256) != 1 {
|
||||
t.Errorf("SHA256 entries: got %d, want 1", len(rf.SHA256))
|
||||
}
|
||||
// PGP signature must not bleed into entries.
|
||||
for _, e := range rf.SHA256 {
|
||||
if strings.Contains(e.Path, "PGP") || strings.Contains(e.Hash, "PGP") {
|
||||
t.Errorf("PGP artifact in entry: %+v", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRelease_parseable(t *testing.T) {
|
||||
entries := []ReleaseEntry{
|
||||
{Hash: "aabbccdd", Size: 100, Path: "main/binary-amd64/Packages"},
|
||||
{Hash: "eeff0011", Size: 200, Path: "main/binary-amd64/Packages.gz"},
|
||||
}
|
||||
data := GenerateRelease("bookworm", "bookworm", []string{"main", "contrib"}, []string{"amd64"}, entries)
|
||||
|
||||
rf, err := ParseRelease(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseRelease on generated output: %v", err)
|
||||
}
|
||||
if rf.Suite != "bookworm" {
|
||||
t.Errorf("Suite: got %q, want bookworm", rf.Suite)
|
||||
}
|
||||
if len(rf.SHA256) != 2 {
|
||||
t.Fatalf("SHA256 entries: got %d, want 2", len(rf.SHA256))
|
||||
}
|
||||
if rf.SHA256[0].Size != 100 {
|
||||
t.Errorf("entry[0] Size: got %d, want 100", rf.SHA256[0].Size)
|
||||
}
|
||||
if rf.SHA256[1].Path != "main/binary-amd64/Packages.gz" {
|
||||
t.Errorf("entry[1] Path: got %q", rf.SHA256[1].Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRelease_emptyEntries(t *testing.T) {
|
||||
data := GenerateRelease("bookworm", "bookworm", []string{"main"}, []string{"amd64"}, nil)
|
||||
rf, err := ParseRelease(data)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(rf.SHA256) != 0 {
|
||||
t.Errorf("expected 0 SHA256 entries, got %d", len(rf.SHA256))
|
||||
}
|
||||
}
|
||||
284
internal/clone/rpm/metadata_gen_test.go
Normal file
284
internal/clone/rpm/metadata_gen_test.go
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
package rpm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// buildPrimaryGz creates a gzipped primary.xml with the given packages.
|
||||
// Each entry is {name, locationHref}.
|
||||
func buildPrimaryGz(t *testing.T, pkgs []struct{ name, href string }) []byte {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||
fmt.Fprintf(&buf, `<metadata xmlns="http://linux.duke.edu/metadata/common" packages="%d">`, len(pkgs))
|
||||
for _, p := range pkgs {
|
||||
fmt.Fprintf(&buf,
|
||||
`<package type="rpm"><name>%s</name><location href="%s"/></package>`,
|
||||
p.name, p.href)
|
||||
}
|
||||
buf.WriteString(`</metadata>`)
|
||||
|
||||
var gz bytes.Buffer
|
||||
gzw := gzip.NewWriter(&gz)
|
||||
gzw.Write(buf.Bytes())
|
||||
gzw.Close()
|
||||
return gz.Bytes()
|
||||
}
|
||||
|
||||
// buildRepoMD creates a minimal repomd.xml pointing to the given primary href.
|
||||
func buildRepoMD(t *testing.T, primaryHref string) []byte {
|
||||
t.Helper()
|
||||
repomd := RepoMD{
|
||||
Data: []RepoMDEntry{{
|
||||
Type: "primary",
|
||||
Location: RepoMDLocation{Href: primaryHref},
|
||||
Checksum: RepoMDChecksum{Type: "sha256", Value: "placeholder"},
|
||||
OpenChecksum: RepoMDChecksum{Type: "sha256", Value: "placeholder"},
|
||||
}},
|
||||
}
|
||||
out, err := xml.MarshalIndent(repomd, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("marshal repomd: %v", err)
|
||||
}
|
||||
return append([]byte(xml.Header), out...)
|
||||
}
|
||||
|
||||
// setupRepo creates a local repo dir with a primary.xml.gz, a .primary-source
|
||||
// marker and a repomd.xml. It creates the given rpm files on disk.
|
||||
func setupRepo(t *testing.T, pkgs []struct{ name, href string }, presentOnDisk []string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
repodata := filepath.Join(dir, "repodata")
|
||||
os.MkdirAll(repodata, 0o755)
|
||||
|
||||
// Upstream primary at a hashed path (mimics real layout).
|
||||
upstreamPrimary := "repodata/upstream-primary.xml.gz"
|
||||
if err := os.WriteFile(filepath.Join(dir, upstreamPrimary), buildPrimaryGz(t, pkgs), 0o644); err != nil {
|
||||
t.Fatalf("write upstream primary: %v", err)
|
||||
}
|
||||
|
||||
// .primary-source marker.
|
||||
if err := os.WriteFile(filepath.Join(repodata, PrimarySourceFile), []byte(upstreamPrimary), 0o644); err != nil {
|
||||
t.Fatalf("write .primary-source: %v", err)
|
||||
}
|
||||
|
||||
// repomd.xml pointing at local primary (which doesn't exist yet — regen will create it).
|
||||
if err := os.WriteFile(filepath.Join(repodata, "repomd.xml"), buildRepoMD(t, localPrimaryHref), 0o644); err != nil {
|
||||
t.Fatalf("write repomd.xml: %v", err)
|
||||
}
|
||||
|
||||
// Create selected rpm files on disk.
|
||||
for _, href := range presentOnDisk {
|
||||
dest := filepath.Join(dir, filepath.FromSlash(href))
|
||||
os.MkdirAll(filepath.Dir(dest), 0o755)
|
||||
if err := os.WriteFile(dest, []byte("fake-rpm"), 0o644); err != nil {
|
||||
t.Fatalf("write rpm %s: %v", href, err)
|
||||
}
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
// parsePrimaryGz decompresses and parses a primary.xml.gz, returning package hrefs.
|
||||
func parsePrimaryGz(t *testing.T, path string) []string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read primary.xml.gz: %v", err)
|
||||
}
|
||||
gz, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
t.Fatalf("open gzip: %v", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
var meta filterableMetadata
|
||||
if err := xml.NewDecoder(gz).Decode(&meta); err != nil {
|
||||
t.Fatalf("parse primary.xml: %v", err)
|
||||
}
|
||||
hrefs := make([]string, len(meta.Packages))
|
||||
for i, p := range meta.Packages {
|
||||
hrefs[i] = p.Location.Href
|
||||
}
|
||||
return hrefs
|
||||
}
|
||||
|
||||
// --- InitEmptyRepo ---
|
||||
|
||||
func TestInitEmptyRepo_createsStructure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := InitEmptyRepo(dir); err != nil {
|
||||
t.Fatalf("InitEmptyRepo: %v", err)
|
||||
}
|
||||
|
||||
for _, path := range []string{
|
||||
filepath.Join(dir, "repodata"),
|
||||
filepath.Join(dir, "Packages"),
|
||||
filepath.Join(dir, "repodata", "repomd.xml"),
|
||||
filepath.Join(dir, localPrimaryHref),
|
||||
} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Errorf("expected %s to exist: %v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitEmptyRepo_repomdIsValid(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
InitEmptyRepo(dir)
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "repodata", "repomd.xml"))
|
||||
var repomd RepoMD
|
||||
if err := xml.Unmarshal(data, &repomd); err != nil {
|
||||
t.Fatalf("repomd.xml is not valid XML: %v", err)
|
||||
}
|
||||
if len(repomd.Data) == 0 {
|
||||
t.Error("expected at least one entry in repomd.xml")
|
||||
}
|
||||
if repomd.Data[0].Type != "primary" {
|
||||
t.Errorf("first entry type: got %q, want primary", repomd.Data[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitEmptyRepo_primaryIsEmpty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
InitEmptyRepo(dir)
|
||||
|
||||
hrefs := parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref))
|
||||
if len(hrefs) != 0 {
|
||||
t.Errorf("expected 0 packages in empty repo, got %d", len(hrefs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitEmptyRepo_idempotent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
InitEmptyRepo(dir)
|
||||
|
||||
repomdBefore, _ := os.ReadFile(filepath.Join(dir, "repodata", "repomd.xml"))
|
||||
InitEmptyRepo(dir) // second call
|
||||
repomdAfter, _ := os.ReadFile(filepath.Join(dir, "repodata", "repomd.xml"))
|
||||
|
||||
if !bytes.Equal(repomdBefore, repomdAfter) {
|
||||
t.Error("second InitEmptyRepo modified repomd.xml")
|
||||
}
|
||||
}
|
||||
|
||||
// --- RegenerateMetadata ---
|
||||
|
||||
func TestRegenerateMetadata_keepsOnlyPresentFiles(t *testing.T) {
|
||||
allPkgs := []struct{ name, href string }{
|
||||
{"curl", "Packages/curl-7.88-1.x86_64.rpm"},
|
||||
{"wget", "Packages/wget-1.21-1.x86_64.rpm"},
|
||||
{"vim", "Packages/vim-9.0-1.x86_64.rpm"},
|
||||
}
|
||||
// Only curl and vim are on disk.
|
||||
presentOnDisk := []string{"Packages/curl-7.88-1.x86_64.rpm", "Packages/vim-9.0-1.x86_64.rpm"}
|
||||
|
||||
dir := setupRepo(t, allPkgs, presentOnDisk)
|
||||
|
||||
if err := RegenerateMetadata(dir); err != nil {
|
||||
t.Fatalf("RegenerateMetadata: %v", err)
|
||||
}
|
||||
|
||||
hrefs := parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref))
|
||||
if len(hrefs) != 2 {
|
||||
t.Fatalf("expected 2 packages, got %d: %v", len(hrefs), hrefs)
|
||||
}
|
||||
hrefSet := map[string]bool{hrefs[0]: true, hrefs[1]: true}
|
||||
if !hrefSet["Packages/curl-7.88-1.x86_64.rpm"] {
|
||||
t.Error("expected curl in output")
|
||||
}
|
||||
if !hrefSet["Packages/vim-9.0-1.x86_64.rpm"] {
|
||||
t.Error("expected vim in output")
|
||||
}
|
||||
if hrefSet["Packages/wget-1.21-1.x86_64.rpm"] {
|
||||
t.Error("wget should have been filtered out (not on disk)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegenerateMetadata_allAbsent(t *testing.T) {
|
||||
allPkgs := []struct{ name, href string }{
|
||||
{"curl", "Packages/curl-7.88-1.x86_64.rpm"},
|
||||
{"wget", "Packages/wget-1.21-1.x86_64.rpm"},
|
||||
}
|
||||
dir := setupRepo(t, allPkgs, nil) // nothing on disk
|
||||
|
||||
if err := RegenerateMetadata(dir); err != nil {
|
||||
t.Fatalf("RegenerateMetadata: %v", err)
|
||||
}
|
||||
|
||||
hrefs := parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref))
|
||||
if len(hrefs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(hrefs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegenerateMetadata_updatesRepoMD(t *testing.T) {
|
||||
allPkgs := []struct{ name, href string }{
|
||||
{"curl", "Packages/curl-7.88-1.x86_64.rpm"},
|
||||
}
|
||||
dir := setupRepo(t, allPkgs, []string{"Packages/curl-7.88-1.x86_64.rpm"})
|
||||
|
||||
if err := RegenerateMetadata(dir); err != nil {
|
||||
t.Fatalf("RegenerateMetadata: %v", err)
|
||||
}
|
||||
|
||||
data, _ := os.ReadFile(filepath.Join(dir, "repodata", "repomd.xml"))
|
||||
var repomd RepoMD
|
||||
xml.Unmarshal(data, &repomd)
|
||||
|
||||
var primary *RepoMDEntry
|
||||
for i := range repomd.Data {
|
||||
if repomd.Data[i].Type == "primary" {
|
||||
primary = &repomd.Data[i]
|
||||
}
|
||||
}
|
||||
if primary == nil {
|
||||
t.Fatal("no primary entry in repomd.xml after regen")
|
||||
}
|
||||
if primary.Location.Href != localPrimaryHref {
|
||||
t.Errorf("Location.Href: got %q, want %q", primary.Location.Href, localPrimaryHref)
|
||||
}
|
||||
if primary.Checksum.Type != "sha256" {
|
||||
t.Errorf("Checksum.Type: got %q, want sha256", primary.Checksum.Type)
|
||||
}
|
||||
if primary.Checksum.Value == "placeholder" {
|
||||
t.Error("checksum was not updated from placeholder")
|
||||
}
|
||||
if primary.Size == 0 {
|
||||
t.Error("Size should be non-zero after regen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegenerateMetadata_alwaysFiltersFromUpstream(t *testing.T) {
|
||||
// Simulates two successive approvals — regen must always read upstream, not the
|
||||
// previously-filtered local primary.
|
||||
allPkgs := []struct{ name, href string }{
|
||||
{"curl", "Packages/curl-7.88-1.x86_64.rpm"},
|
||||
{"wget", "Packages/wget-1.21-1.x86_64.rpm"},
|
||||
{"vim", "Packages/vim-9.0-1.x86_64.rpm"},
|
||||
}
|
||||
dir := setupRepo(t, allPkgs, []string{"Packages/curl-7.88-1.x86_64.rpm"})
|
||||
|
||||
// First regen: only curl on disk.
|
||||
RegenerateMetadata(dir)
|
||||
hrefs := parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref))
|
||||
if len(hrefs) != 1 {
|
||||
t.Fatalf("after first regen: expected 1, got %d", len(hrefs))
|
||||
}
|
||||
|
||||
// Add wget to disk, regen again — vim still absent.
|
||||
dest := filepath.Join(dir, "Packages", "wget-1.21-1.x86_64.rpm")
|
||||
os.WriteFile(dest, []byte("fake"), 0o644)
|
||||
|
||||
RegenerateMetadata(dir)
|
||||
hrefs = parsePrimaryGz(t, filepath.Join(dir, localPrimaryHref))
|
||||
if len(hrefs) != 2 {
|
||||
t.Fatalf("after second regen: expected 2, got %d: %v", len(hrefs), hrefs)
|
||||
}
|
||||
}
|
||||
355
internal/core/repo_test.go
Normal file
355
internal/core/repo_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
417
internal/core/snapshot_test.go
Normal file
417
internal/core/snapshot_test.go
Normal 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
268
internal/core/sync_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
131
internal/store/blocked_test.go
Normal file
131
internal/store/blocked_test.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
func newBlockedStore(t *testing.T) (*store.SQLiteBlockedPackageStore, int64) {
|
||||
t.Helper()
|
||||
db := newTestDB(t)
|
||||
repoID := insertTestRepo(t, db)
|
||||
return store.NewBlockedPackageStore(db), repoID
|
||||
}
|
||||
|
||||
func TestBlockAndListPackages(t *testing.T) {
|
||||
s, repoID := newBlockedStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
pkgs := []store.BlockedPackage{
|
||||
{Name: "curl", Location: "Packages/curl-7.88.rpm"},
|
||||
{Name: "wget", Location: "Packages/wget-1.21.rpm"},
|
||||
}
|
||||
if err := s.BlockPackages(ctx, repoID, pkgs); err != nil {
|
||||
t.Fatalf("BlockPackages: %v", err)
|
||||
}
|
||||
|
||||
listed, err := s.ListBlocked(ctx, repoID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListBlocked: %v", err)
|
||||
}
|
||||
if len(listed) != 2 {
|
||||
t.Fatalf("expected 2 blocked, got %d", len(listed))
|
||||
}
|
||||
if listed[0].Name != "curl" {
|
||||
t.Errorf("expected curl first, got %q", listed[0].Name)
|
||||
}
|
||||
if listed[0].RepoID != repoID {
|
||||
t.Errorf("RepoID: got %d, want %d", listed[0].RepoID, repoID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockPackages_idempotent(t *testing.T) {
|
||||
s, repoID := newBlockedStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := []store.BlockedPackage{{Name: "curl", Location: "Packages/curl.rpm"}}
|
||||
s.BlockPackages(ctx, repoID, pkg)
|
||||
// INSERT OR IGNORE — should not duplicate.
|
||||
s.BlockPackages(ctx, repoID, pkg)
|
||||
|
||||
listed, _ := s.ListBlocked(ctx, repoID)
|
||||
if len(listed) != 1 {
|
||||
t.Errorf("expected 1 after double block, got %d", len(listed))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnblockPackages(t *testing.T) {
|
||||
s, repoID := newBlockedStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
s.BlockPackages(ctx, repoID, []store.BlockedPackage{
|
||||
{Name: "curl", Location: "Packages/curl.rpm"},
|
||||
{Name: "wget", Location: "Packages/wget.rpm"},
|
||||
})
|
||||
|
||||
listed, _ := s.ListBlocked(ctx, repoID)
|
||||
if len(listed) != 2 {
|
||||
t.Fatalf("setup: expected 2, got %d", len(listed))
|
||||
}
|
||||
|
||||
// Unblock only the first.
|
||||
if err := s.UnblockPackages(ctx, repoID, []int64{listed[0].ID}); err != nil {
|
||||
t.Fatalf("UnblockPackages: %v", err)
|
||||
}
|
||||
|
||||
remaining, _ := s.ListBlocked(ctx, repoID)
|
||||
if len(remaining) != 1 {
|
||||
t.Fatalf("expected 1 remaining, got %d", len(remaining))
|
||||
}
|
||||
if remaining[0].Name != "wget" {
|
||||
t.Errorf("expected wget remaining, got %q", remaining[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnblockPackages_empty(t *testing.T) {
|
||||
s, repoID := newBlockedStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := s.UnblockPackages(ctx, repoID, nil); err != nil {
|
||||
t.Errorf("UnblockPackages(nil): %v", err)
|
||||
}
|
||||
if err := s.UnblockPackages(ctx, repoID, []int64{}); err != nil {
|
||||
t.Errorf("UnblockPackages([]): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListBlocked_empty(t *testing.T) {
|
||||
s, repoID := newBlockedStore(t)
|
||||
listed, err := s.ListBlocked(context.Background(), repoID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListBlocked: %v", err)
|
||||
}
|
||||
if len(listed) != 0 {
|
||||
t.Errorf("expected 0, got %d", len(listed))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockPackages_isolation(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
repoStore := store.NewRepoStore(db)
|
||||
ctx := context.Background()
|
||||
|
||||
id1, _ := repoStore.CreateRepo(ctx, &store.Repo{Name: "repo-1", Type: "rpm", SourceURL: "https://a.com", SyncMode: "auto"})
|
||||
id2, _ := repoStore.CreateRepo(ctx, &store.Repo{Name: "repo-2", Type: "rpm", SourceURL: "https://b.com", SyncMode: "auto"})
|
||||
|
||||
bs := store.NewBlockedPackageStore(db)
|
||||
bs.BlockPackages(ctx, id1, []store.BlockedPackage{{Name: "curl", Location: "pkg/curl.rpm"}})
|
||||
bs.BlockPackages(ctx, id2, []store.BlockedPackage{{Name: "vim", Location: "pkg/vim.rpm"}})
|
||||
|
||||
list1, _ := bs.ListBlocked(ctx, id1)
|
||||
list2, _ := bs.ListBlocked(ctx, id2)
|
||||
|
||||
if len(list1) != 1 || list1[0].Name != "curl" {
|
||||
t.Errorf("repo-1: expected [curl], got %v", list1)
|
||||
}
|
||||
if len(list2) != 1 || list2[0].Name != "vim" {
|
||||
t.Errorf("repo-2: expected [vim], got %v", list2)
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ func (s *SQLiteCloneJobStore) GetCloneJob(ctx context.Context, id int64) (*Clone
|
|||
|
||||
func (s *SQLiteCloneJobStore) GetLatestCloneJob(ctx context.Context, repoID int64) (*CloneJob, error) {
|
||||
row := s.db.QueryRowContext(ctx,
|
||||
`SELECT id, repo_id, status, started_at, finished_at, error, created_at FROM clone_jobs WHERE repo_id = ? ORDER BY created_at DESC LIMIT 1`, repoID)
|
||||
`SELECT id, repo_id, status, started_at, finished_at, error, created_at FROM clone_jobs WHERE repo_id = ? ORDER BY created_at DESC, id DESC LIMIT 1`, repoID)
|
||||
return scanCloneJob(row)
|
||||
}
|
||||
|
||||
|
|
|
|||
165
internal/store/clone_job_test.go
Normal file
165
internal/store/clone_job_test.go
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
func newCloneJobStore(t *testing.T) (*store.SQLiteCloneJobStore, int64) {
|
||||
t.Helper()
|
||||
db := newTestDB(t)
|
||||
repoID := insertTestRepo(t, db)
|
||||
return store.NewCloneJobStore(db), repoID
|
||||
}
|
||||
|
||||
func TestCreateAndGetCloneJob(t *testing.T) {
|
||||
s, repoID := newCloneJobStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
jobID, err := s.CreateCloneJob(ctx, repoID)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCloneJob: %v", err)
|
||||
}
|
||||
|
||||
job, err := s.GetCloneJob(ctx, jobID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCloneJob: %v", err)
|
||||
}
|
||||
if job.RepoID != repoID {
|
||||
t.Errorf("RepoID: got %d, want %d", job.RepoID, repoID)
|
||||
}
|
||||
if job.Status != store.CloneJobPending {
|
||||
t.Errorf("Status: got %q, want pending", job.Status)
|
||||
}
|
||||
if job.StartedAt != nil {
|
||||
t.Errorf("StartedAt should be nil initially")
|
||||
}
|
||||
if job.FinishedAt != nil {
|
||||
t.Errorf("FinishedAt should be nil initially")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCloneJob_notFound(t *testing.T) {
|
||||
s, _ := newCloneJobStore(t)
|
||||
_, err := s.GetCloneJob(context.Background(), 9999)
|
||||
if err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLatestCloneJob(t *testing.T) {
|
||||
s, repoID := newCloneJobStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
s.CreateCloneJob(ctx, repoID)
|
||||
id2, _ := s.CreateCloneJob(ctx, repoID)
|
||||
|
||||
job, err := s.GetLatestCloneJob(ctx, repoID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLatestCloneJob: %v", err)
|
||||
}
|
||||
if job.ID != id2 {
|
||||
t.Errorf("expected latest job ID %d, got %d", id2, job.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLatestCloneJob_notFound(t *testing.T) {
|
||||
s, repoID := newCloneJobStore(t)
|
||||
_, err := s.GetLatestCloneJob(context.Background(), repoID)
|
||||
if err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkCloneJobStarted(t *testing.T) {
|
||||
s, repoID := newCloneJobStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
jobID, _ := s.CreateCloneJob(ctx, repoID)
|
||||
if err := s.MarkCloneJobStarted(ctx, jobID); err != nil {
|
||||
t.Fatalf("MarkCloneJobStarted: %v", err)
|
||||
}
|
||||
|
||||
job, _ := s.GetCloneJob(ctx, jobID)
|
||||
if job.Status != store.CloneJobRunning {
|
||||
t.Errorf("Status: got %q, want running", job.Status)
|
||||
}
|
||||
if job.StartedAt == nil {
|
||||
t.Error("StartedAt should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkCloneJobFinished_completed(t *testing.T) {
|
||||
s, repoID := newCloneJobStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
jobID, _ := s.CreateCloneJob(ctx, repoID)
|
||||
s.MarkCloneJobStarted(ctx, jobID)
|
||||
if err := s.MarkCloneJobFinished(ctx, jobID, store.CloneJobCompleted, nil); err != nil {
|
||||
t.Fatalf("MarkCloneJobFinished: %v", err)
|
||||
}
|
||||
|
||||
job, _ := s.GetCloneJob(ctx, jobID)
|
||||
if job.Status != store.CloneJobCompleted {
|
||||
t.Errorf("Status: got %q, want completed", job.Status)
|
||||
}
|
||||
if job.FinishedAt == nil {
|
||||
t.Error("FinishedAt should be set")
|
||||
}
|
||||
if job.Error != nil {
|
||||
t.Errorf("Error should be nil, got %q", *job.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkCloneJobFinished_failed(t *testing.T) {
|
||||
s, repoID := newCloneJobStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
jobID, _ := s.CreateCloneJob(ctx, repoID)
|
||||
s.MarkCloneJobStarted(ctx, jobID)
|
||||
errMsg := "connection refused"
|
||||
if err := s.MarkCloneJobFinished(ctx, jobID, store.CloneJobFailed, &errMsg); err != nil {
|
||||
t.Fatalf("MarkCloneJobFinished: %v", err)
|
||||
}
|
||||
|
||||
job, _ := s.GetCloneJob(ctx, jobID)
|
||||
if job.Status != store.CloneJobFailed {
|
||||
t.Errorf("Status: got %q, want failed", job.Status)
|
||||
}
|
||||
if job.Error == nil || *job.Error != errMsg {
|
||||
t.Errorf("Error: got %v, want %q", job.Error, errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasRunningCloneJob(t *testing.T) {
|
||||
s, repoID := newCloneJobStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
running, err := s.HasRunningCloneJob(ctx, repoID)
|
||||
if err != nil {
|
||||
t.Fatalf("HasRunningCloneJob: %v", err)
|
||||
}
|
||||
if running {
|
||||
t.Error("expected no running job initially")
|
||||
}
|
||||
|
||||
jobID, _ := s.CreateCloneJob(ctx, repoID)
|
||||
running, _ = s.HasRunningCloneJob(ctx, repoID)
|
||||
if !running {
|
||||
t.Error("expected running job after create (status=pending)")
|
||||
}
|
||||
|
||||
s.MarkCloneJobStarted(ctx, jobID)
|
||||
running, _ = s.HasRunningCloneJob(ctx, repoID)
|
||||
if !running {
|
||||
t.Error("expected running job after start")
|
||||
}
|
||||
|
||||
s.MarkCloneJobFinished(ctx, jobID, store.CloneJobCompleted, nil)
|
||||
running, _ = s.HasRunningCloneJob(ctx, repoID)
|
||||
if running {
|
||||
t.Error("expected no running job after completion")
|
||||
}
|
||||
}
|
||||
139
internal/store/pending_test.go
Normal file
139
internal/store/pending_test.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
func newPendingStore(t *testing.T) (*store.SQLitePendingPackageStore, int64) {
|
||||
t.Helper()
|
||||
db := newTestDB(t)
|
||||
repoID := insertTestRepo(t, db)
|
||||
return store.NewPendingPackageStore(db), repoID
|
||||
}
|
||||
|
||||
func makePending(repoID int64, name, version, location string) store.PendingPackage {
|
||||
return store.PendingPackage{
|
||||
RepoID: repoID,
|
||||
Name: name,
|
||||
Version: version,
|
||||
Arch: "amd64",
|
||||
Location: location,
|
||||
Checksum: "abc123",
|
||||
ChecksumType: "sha256",
|
||||
Size: 1024,
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertAndListPending(t *testing.T) {
|
||||
s, repoID := newPendingStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
pkgs := []store.PendingPackage{
|
||||
makePending(repoID, "curl", "7.88", "Packages/curl.rpm"),
|
||||
makePending(repoID, "wget", "1.21", "Packages/wget.rpm"),
|
||||
}
|
||||
if err := s.UpsertPending(ctx, pkgs); err != nil {
|
||||
t.Fatalf("UpsertPending: %v", err)
|
||||
}
|
||||
|
||||
listed, err := s.ListPending(ctx, repoID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListPending: %v", err)
|
||||
}
|
||||
if len(listed) != 2 {
|
||||
t.Fatalf("expected 2 pending, got %d", len(listed))
|
||||
}
|
||||
if listed[0].Name != "curl" {
|
||||
t.Errorf("expected curl first, got %q", listed[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertPending_idempotent(t *testing.T) {
|
||||
s, repoID := newPendingStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := makePending(repoID, "curl", "7.88", "Packages/curl.rpm")
|
||||
s.UpsertPending(ctx, []store.PendingPackage{pkg})
|
||||
// Insert same package again — INSERT OR IGNORE, should not duplicate.
|
||||
s.UpsertPending(ctx, []store.PendingPackage{pkg})
|
||||
|
||||
listed, _ := s.ListPending(ctx, repoID)
|
||||
if len(listed) != 1 {
|
||||
t.Errorf("expected 1 after double upsert, got %d", len(listed))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePending_byIDs(t *testing.T) {
|
||||
s, repoID := newPendingStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
pkgs := []store.PendingPackage{
|
||||
makePending(repoID, "curl", "7.88", "Packages/curl.rpm"),
|
||||
makePending(repoID, "wget", "1.21", "Packages/wget.rpm"),
|
||||
makePending(repoID, "vim", "9.0", "Packages/vim.rpm"),
|
||||
}
|
||||
s.UpsertPending(ctx, pkgs)
|
||||
|
||||
listed, _ := s.ListPending(ctx, repoID)
|
||||
if len(listed) != 3 {
|
||||
t.Fatalf("setup: expected 3, got %d", len(listed))
|
||||
}
|
||||
|
||||
// Delete first two.
|
||||
ids := []int64{listed[0].ID, listed[1].ID}
|
||||
if err := s.DeletePending(ctx, ids); err != nil {
|
||||
t.Fatalf("DeletePending: %v", err)
|
||||
}
|
||||
|
||||
remaining, _ := s.ListPending(ctx, 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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePending_empty(t *testing.T) {
|
||||
s, _ := newPendingStore(t)
|
||||
// Should not error on empty slice.
|
||||
if err := s.DeletePending(context.Background(), nil); err != nil {
|
||||
t.Errorf("DeletePending(nil): %v", err)
|
||||
}
|
||||
if err := s.DeletePending(context.Background(), []int64{}); err != nil {
|
||||
t.Errorf("DeletePending([]): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAllPending(t *testing.T) {
|
||||
s, repoID := newPendingStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
s.UpsertPending(ctx, []store.PendingPackage{
|
||||
makePending(repoID, "curl", "7.88", "Packages/curl.rpm"),
|
||||
makePending(repoID, "wget", "1.21", "Packages/wget.rpm"),
|
||||
})
|
||||
|
||||
if err := s.DeleteAllPending(ctx, repoID); err != nil {
|
||||
t.Fatalf("DeleteAllPending: %v", err)
|
||||
}
|
||||
|
||||
remaining, _ := s.ListPending(ctx, repoID)
|
||||
if len(remaining) != 0 {
|
||||
t.Errorf("expected 0 after DeleteAll, got %d", len(remaining))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPending_empty(t *testing.T) {
|
||||
s, repoID := newPendingStore(t)
|
||||
listed, err := s.ListPending(context.Background(), repoID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListPending: %v", err)
|
||||
}
|
||||
if len(listed) != 0 {
|
||||
t.Errorf("expected 0, got %d", len(listed))
|
||||
}
|
||||
}
|
||||
188
internal/store/repo_test.go
Normal file
188
internal/store/repo_test.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
func newTestStore(t *testing.T) *store.SQLiteRepoStore {
|
||||
t.Helper()
|
||||
return store.NewRepoStore(newTestDB(t))
|
||||
}
|
||||
|
||||
func TestCreateAndGetRepo(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
r := &store.Repo{Name: "test-repo", Type: "rpm", SourceURL: "https://example.com", SyncMode: "auto"}
|
||||
id, err := s.CreateRepo(ctx, r)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRepo: %v", err)
|
||||
}
|
||||
if id <= 0 {
|
||||
t.Errorf("expected positive id, got %d", id)
|
||||
}
|
||||
|
||||
got, err := s.GetRepo(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRepo: %v", err)
|
||||
}
|
||||
if got.Name != "test-repo" {
|
||||
t.Errorf("Name: got %q, want %q", got.Name, "test-repo")
|
||||
}
|
||||
if got.Type != "rpm" {
|
||||
t.Errorf("Type: got %q, want %q", got.Type, "rpm")
|
||||
}
|
||||
if got.SourceURL != "https://example.com" {
|
||||
t.Errorf("SourceURL: got %q", got.SourceURL)
|
||||
}
|
||||
if got.SyncMode != "auto" {
|
||||
t.Errorf("SyncMode: got %q, want auto", got.SyncMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepo_notFound(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := s.GetRepo(ctx, 9999)
|
||||
if err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepoByName(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
r := &store.Repo{Name: "debian-bookworm", Type: "apt", SourceURL: "https://deb.debian.org/debian", SyncMode: "manual", Config: `{"suite":"bookworm"}`}
|
||||
id, err := s.CreateRepo(ctx, r)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRepo: %v", err)
|
||||
}
|
||||
|
||||
got, err := s.GetRepoByName(ctx, "debian-bookworm")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRepoByName: %v", err)
|
||||
}
|
||||
if got.ID != id {
|
||||
t.Errorf("ID: got %d, want %d", got.ID, id)
|
||||
}
|
||||
if got.Config != `{"suite":"bookworm"}` {
|
||||
t.Errorf("Config: got %q", got.Config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepoByName_notFound(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := s.GetRepoByName(ctx, "does-not-exist")
|
||||
if err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRepos(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
repos, err := s.ListRepos(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRepos (empty): %v", err)
|
||||
}
|
||||
if len(repos) != 0 {
|
||||
t.Errorf("expected empty list, got %d repos", len(repos))
|
||||
}
|
||||
|
||||
for _, name := range []string{"repo-a", "repo-b", "repo-c"} {
|
||||
_, err := s.CreateRepo(ctx, &store.Repo{Name: name, Type: "rpm", SourceURL: "https://x.com", SyncMode: "auto"})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRepo %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
repos, err = s.ListRepos(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRepos: %v", err)
|
||||
}
|
||||
if len(repos) != 3 {
|
||||
t.Errorf("expected 3 repos, got %d", len(repos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRepo(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
id, _ := s.CreateRepo(ctx, &store.Repo{Name: "to-delete", Type: "rpm", SourceURL: "https://x.com", SyncMode: "auto"})
|
||||
|
||||
if err := s.DeleteRepo(ctx, id); err != nil {
|
||||
t.Fatalf("DeleteRepo: %v", err)
|
||||
}
|
||||
_, err := s.GetRepo(ctx, id)
|
||||
if err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound after delete, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRepo_notFound(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
err := s.DeleteRepo(ctx, 9999)
|
||||
if err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRepo(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
id, _ := s.CreateRepo(ctx, &store.Repo{Name: "updatable", Type: "apt", SourceURL: "https://old.example.com", SyncMode: "auto", Config: ""})
|
||||
|
||||
newConfig := `{"suite":"bookworm","components":["main"],"architectures":["amd64"]}`
|
||||
if err := s.UpdateRepo(ctx, id, "https://new.example.com", "manual", newConfig); err != nil {
|
||||
t.Fatalf("UpdateRepo: %v", err)
|
||||
}
|
||||
|
||||
got, err := s.GetRepo(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRepo after update: %v", err)
|
||||
}
|
||||
if got.SourceURL != "https://new.example.com" {
|
||||
t.Errorf("SourceURL: got %q, want https://new.example.com", got.SourceURL)
|
||||
}
|
||||
if got.SyncMode != "manual" {
|
||||
t.Errorf("SyncMode: got %q, want manual", got.SyncMode)
|
||||
}
|
||||
if got.Config != newConfig {
|
||||
t.Errorf("Config: got %q", got.Config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRepo_notFound(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
err := s.UpdateRepo(ctx, 9999, "https://x.com", "auto", "")
|
||||
if err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRepo_uniqueName(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
r := &store.Repo{Name: "unique", Type: "rpm", SourceURL: "https://x.com", SyncMode: "auto"}
|
||||
if _, err := s.CreateRepo(ctx, r); err != nil {
|
||||
t.Fatalf("first create: %v", err)
|
||||
}
|
||||
if _, err := s.CreateRepo(ctx, r); err == nil {
|
||||
t.Error("expected error for duplicate name, got nil")
|
||||
}
|
||||
}
|
||||
142
internal/store/snapshot_test.go
Normal file
142
internal/store/snapshot_test.go
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
func newSnapshotStore(t *testing.T) (*store.SQLiteSnapshotStore, int64) {
|
||||
t.Helper()
|
||||
db := newTestDB(t)
|
||||
repoID := insertTestRepo(t, db)
|
||||
return store.NewSnapshotStore(db), repoID
|
||||
}
|
||||
|
||||
func TestCreateAndGetSnapshot(t *testing.T) {
|
||||
s, repoID := newSnapshotStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
id, err := s.CreateSnapshot(ctx, repoID, "v1.0")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSnapshot: %v", err)
|
||||
}
|
||||
|
||||
snap, err := s.GetSnapshot(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSnapshot: %v", err)
|
||||
}
|
||||
if snap.RepoID != repoID {
|
||||
t.Errorf("RepoID: got %d, want %d", snap.RepoID, repoID)
|
||||
}
|
||||
if snap.Label != "v1.0" {
|
||||
t.Errorf("Label: got %q, want v1.0", snap.Label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSnapshot_notFound(t *testing.T) {
|
||||
s, _ := newSnapshotStore(t)
|
||||
_, err := s.GetSnapshot(context.Background(), 9999)
|
||||
if err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSnapshots(t *testing.T) {
|
||||
s, repoID := newSnapshotStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
snaps, err := s.ListSnapshots(ctx, repoID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSnapshots (empty): %v", err)
|
||||
}
|
||||
if len(snaps) != 0 {
|
||||
t.Errorf("expected 0 snapshots, got %d", len(snaps))
|
||||
}
|
||||
|
||||
s.CreateSnapshot(ctx, repoID, "snap-1")
|
||||
s.CreateSnapshot(ctx, repoID, "snap-2")
|
||||
|
||||
snaps, err = s.ListSnapshots(ctx, repoID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListSnapshots: %v", err)
|
||||
}
|
||||
if len(snaps) != 2 {
|
||||
t.Fatalf("expected 2 snapshots, got %d", len(snaps))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddAndGetSnapshotPackages(t *testing.T) {
|
||||
s, repoID := newSnapshotStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
snapID, _ := s.CreateSnapshot(ctx, repoID, "with-pkgs")
|
||||
|
||||
pkgs := []store.SnapshotPackage{
|
||||
{Name: "curl", Version: "7.88", Arch: "amd64", Location: "Packages/curl.rpm", Checksum: "abc", ChecksumType: "sha256", Size: 1000},
|
||||
{Name: "wget", Version: "1.21", Arch: "amd64", Location: "Packages/wget.rpm", Checksum: "def", ChecksumType: "sha256", Size: 2000},
|
||||
}
|
||||
if err := s.AddSnapshotPackages(ctx, snapID, pkgs); err != nil {
|
||||
t.Fatalf("AddSnapshotPackages: %v", err)
|
||||
}
|
||||
|
||||
got, err := s.GetSnapshotPackages(ctx, snapID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSnapshotPackages: %v", err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 packages, got %d", len(got))
|
||||
}
|
||||
if got[0].Name != "curl" {
|
||||
t.Errorf("pkg[0].Name: got %q, want curl", got[0].Name)
|
||||
}
|
||||
if got[1].Size != 2000 {
|
||||
t.Errorf("pkg[1].Size: got %d, want 2000", got[1].Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSnapshotPackages_empty(t *testing.T) {
|
||||
s, repoID := newSnapshotStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
snapID, _ := s.CreateSnapshot(ctx, repoID, "empty")
|
||||
if err := s.AddSnapshotPackages(ctx, snapID, nil); err != nil {
|
||||
t.Fatalf("AddSnapshotPackages with nil: %v", err)
|
||||
}
|
||||
pkgs, _ := s.GetSnapshotPackages(ctx, snapID)
|
||||
if len(pkgs) != 0 {
|
||||
t.Errorf("expected 0 packages, got %d", len(pkgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSnapshot(t *testing.T) {
|
||||
s, repoID := newSnapshotStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
snapID, _ := s.CreateSnapshot(ctx, repoID, "to-delete")
|
||||
pkgs := []store.SnapshotPackage{
|
||||
{Name: "curl", Version: "7.88", Arch: "amd64", Location: "loc", Checksum: "abc", ChecksumType: "sha256", Size: 100},
|
||||
}
|
||||
s.AddSnapshotPackages(ctx, snapID, pkgs)
|
||||
|
||||
if err := s.DeleteSnapshot(ctx, snapID); err != nil {
|
||||
t.Fatalf("DeleteSnapshot: %v", err)
|
||||
}
|
||||
if _, err := s.GetSnapshot(ctx, snapID); err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound after delete, got %v", err)
|
||||
}
|
||||
// Cascade: packages should also be gone.
|
||||
remaining, _ := s.GetSnapshotPackages(ctx, snapID)
|
||||
if len(remaining) != 0 {
|
||||
t.Errorf("expected cascade delete of packages, got %d", len(remaining))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSnapshot_notFound(t *testing.T) {
|
||||
s, _ := newSnapshotStore(t)
|
||||
err := s.DeleteSnapshot(context.Background(), 9999)
|
||||
if err != store.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
36
internal/store/testhelper_test.go
Normal file
36
internal/store/testhelper_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/syonad/clonepack/config"
|
||||
"github.com/syonad/clonepack/internal/store"
|
||||
)
|
||||
|
||||
func newTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
db, err := store.Open(config.DBConfig{Path: ":memory:"})
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
return db
|
||||
}
|
||||
|
||||
// insertTestRepo crée un repo minimal et retourne son ID.
|
||||
func insertTestRepo(t *testing.T, db *sql.DB) int64 {
|
||||
t.Helper()
|
||||
s := store.NewRepoStore(db)
|
||||
id, err := s.CreateRepo(context.Background(), &store.Repo{
|
||||
Name: "test-repo",
|
||||
Type: "rpm",
|
||||
SourceURL: "https://example.com",
|
||||
SyncMode: "auto",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("insertTestRepo: %v", err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue