urapt is a self-hostable APT repository server with a companion CLI for pushing and managing Debian .deb packages. Server (urapt-server): - REST API + APT endpoint, SQLite storage (pure-Go modernc driver, no CGO) - .deb files stored content-addressed on disk, reference-counted for dedup - Server-managed RSA-4096 OpenPGP signing key (ProtonMail/go-crypto) - APT indices (Release/InRelease/Packages[.gz/.xz]) generated on demand from the DB, cached in memory, signed with the server key - Full APT model: repositories -> distributions -> components -> architectures - Bearer-token auth for REST; HTTP Basic auth for private-repo APT reads - First registrant becomes admin; repo-scoped permissions (read/write/read-write/admin) plus owner and server-admin roles - Multipart package push with control-field extraction, list/show/delete, pool serving, blob ref-count cleanup - Audit log CLI (urapt): - register/login/logout/whoami, token management - repo/distro/component/arch CRUD, member management - push/pull/ls/show/rm for packages - apt-config helper that emits apt setup commands (key, sources.list, auth.conf for private repos) Packaging & docs: - Dockerfile (multi-stage distroless), docker-compose.yml, sample config - README quick start, architecture overview, config reference, security notes - PLAN.md design blueprint, CHANGELOG.md, GPL-3.0 LICENSE - GitHub Actions CI (test, lint, cross-build for linux/darwin amd64/arm64) - Makefile release target producing static binaries + tarballs + checksums Tests cover the data-access layer, auth/permission checks, APT index generation, .deb parsing, GPG signing, the REST API, and the typed API client. Verified end-to-end on a Raspberry Pi (arm64) pushing and installing a real package.
205 lines
5.6 KiB
Go
205 lines
5.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// --- distributions ---
|
|
|
|
func TestCreateDistributionAndGetByName(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
u := s.createUser(t, ctx, "alice", "p")
|
|
repo := s.createRepo(t, ctx, "repo", u.ID, "public")
|
|
|
|
d := s.createDistro(t, ctx, repo.ID, "stable")
|
|
if d.Name != "stable" || d.RepositoryID != repo.ID {
|
|
t.Fatalf("distro = %+v", d)
|
|
}
|
|
|
|
got, err := s.GetDistributionByName(ctx, repo.ID, "stable")
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if got.ID != d.ID {
|
|
t.Fatal("id mismatch")
|
|
}
|
|
if _, err := s.GetDistributionByName(ctx, repo.ID, "missing"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("expected ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListDistributions(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
u := s.createUser(t, ctx, "alice", "p")
|
|
repo := s.createRepo(t, ctx, "repo", u.ID, "public")
|
|
s.createDistro(t, ctx, repo.ID, "stable")
|
|
s.createDistro(t, ctx, repo.ID, "unstable")
|
|
s.createDistro(t, ctx, repo.ID, "oldstable")
|
|
|
|
ds, err := s.ListDistributions(ctx, repo.ID)
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
if len(ds) != 3 {
|
|
t.Fatalf("expected 3 distros, got %d", len(ds))
|
|
}
|
|
// Ordered by name.
|
|
if ds[0].Name != "oldstable" || ds[1].Name != "stable" || ds[2].Name != "unstable" {
|
|
names := []string{ds[0].Name, ds[1].Name, ds[2].Name}
|
|
t.Fatalf("expected sorted, got %v", names)
|
|
}
|
|
}
|
|
|
|
func TestDeleteDistribution(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
u := s.createUser(t, ctx, "alice", "p")
|
|
repo := s.createRepo(t, ctx, "repo", u.ID, "public")
|
|
s.createDistro(t, ctx, repo.ID, "stable")
|
|
|
|
if err := s.DeleteDistribution(ctx, repo.ID, "stable"); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
if _, err := s.GetDistributionByName(ctx, repo.ID, "stable"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("expected ErrNotFound after delete, got %v", err)
|
|
}
|
|
if err := s.DeleteDistribution(ctx, repo.ID, "stable"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("second delete should be ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
// --- components ---
|
|
|
|
func TestCreateComponentAndList(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
u := s.createUser(t, ctx, "alice", "p")
|
|
repo := s.createRepo(t, ctx, "repo", u.ID, "public")
|
|
d := s.createDistro(t, ctx, repo.ID, "stable")
|
|
|
|
s.createComponent(t, ctx, d.ID, "main")
|
|
s.createComponent(t, ctx, d.ID, "contrib")
|
|
s.createComponent(t, ctx, d.ID, "non-free")
|
|
|
|
cs, err := s.ListComponents(ctx, d.ID)
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
if len(cs) != 3 {
|
|
t.Fatalf("expected 3 components, got %d", len(cs))
|
|
}
|
|
if cs[0].Name != "contrib" {
|
|
t.Fatalf("expected sorted; first = %q", cs[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestDeleteComponent(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
u := s.createUser(t, ctx, "alice", "p")
|
|
repo := s.createRepo(t, ctx, "repo", u.ID, "public")
|
|
d := s.createDistro(t, ctx, repo.ID, "stable")
|
|
s.createComponent(t, ctx, d.ID, "main")
|
|
|
|
if err := s.DeleteComponent(ctx, d.ID, "main"); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
if err := s.DeleteComponent(ctx, d.ID, "main"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("second delete should be ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCountComponentsByDistro(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
u := s.createUser(t, ctx, "alice", "p")
|
|
repo := s.createRepo(t, ctx, "repo", u.ID, "public")
|
|
d := s.createDistro(t, ctx, repo.ID, "stable")
|
|
|
|
n, err := s.CountComponentsByDistro(ctx, d.ID)
|
|
if err != nil {
|
|
t.Fatalf("count: %v", err)
|
|
}
|
|
if n != 0 {
|
|
t.Fatalf("expected 0, got %d", n)
|
|
}
|
|
s.createComponent(t, ctx, d.ID, "main")
|
|
s.createComponent(t, ctx, d.ID, "contrib")
|
|
n, _ = s.CountComponentsByDistro(ctx, d.ID)
|
|
if n != 2 {
|
|
t.Fatalf("expected 2, got %d", n)
|
|
}
|
|
}
|
|
|
|
// --- architectures ---
|
|
|
|
func TestCreateArchitectureAndList(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
u := s.createUser(t, ctx, "alice", "p")
|
|
repo := s.createRepo(t, ctx, "repo", u.ID, "public")
|
|
d := s.createDistro(t, ctx, repo.ID, "stable")
|
|
|
|
if _, err := s.CreateArchitecture(ctx, d.ID, "amd64"); err != nil {
|
|
t.Fatalf("create amd64: %v", err)
|
|
}
|
|
if _, err := s.CreateArchitecture(ctx, d.ID, "arm64"); err != nil {
|
|
t.Fatalf("create arm64: %v", err)
|
|
}
|
|
|
|
arches, err := s.ListArchitectures(ctx, d.ID)
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
if len(arches) != 2 {
|
|
t.Fatalf("expected 2 arches, got %d", len(arches))
|
|
}
|
|
if arches[0].Name != "amd64" || arches[1].Name != "arm64" {
|
|
t.Fatalf("expected sorted, got %q %q", arches[0].Name, arches[1].Name)
|
|
}
|
|
}
|
|
|
|
func TestDeleteArchitecture(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
u := s.createUser(t, ctx, "alice", "p")
|
|
repo := s.createRepo(t, ctx, "repo", u.ID, "public")
|
|
d := s.createDistro(t, ctx, repo.ID, "stable")
|
|
_, _ = s.CreateArchitecture(ctx, d.ID, "amd64")
|
|
|
|
if err := s.DeleteArchitecture(ctx, d.ID, "amd64"); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
if err := s.DeleteArchitecture(ctx, d.ID, "amd64"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("second delete should be ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHasArchitecture(t *testing.T) {
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
u := s.createUser(t, ctx, "alice", "p")
|
|
repo := s.createRepo(t, ctx, "repo", u.ID, "public")
|
|
d := s.createDistro(t, ctx, repo.ID, "stable")
|
|
_, _ = s.CreateArchitecture(ctx, d.ID, "amd64")
|
|
|
|
has, err := s.HasArchitecture(ctx, d.ID, "amd64")
|
|
if err != nil {
|
|
t.Fatalf("has amd64: %v", err)
|
|
}
|
|
if !has {
|
|
t.Fatal("expected has=true for amd64")
|
|
}
|
|
has, err = s.HasArchitecture(ctx, d.ID, "arm64")
|
|
if err != nil {
|
|
t.Fatalf("has arm64: %v", err)
|
|
}
|
|
if has {
|
|
t.Fatal("expected has=false for arm64")
|
|
}
|
|
}
|