add test for the wall app

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

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