354 lines
10 KiB
Go
354 lines
10 KiB
Go
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)
|
|
}
|
|
}
|