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", }) // Initialise a proper RPM repo so RegenerateMetadata and TakeSnapshot succeed. localDir := filepath.Join(env.dataDir, "repos", "1", "rpm") buildRPMRepo(t, localDir, nil) // 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)) } }