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) } }