Files
pages/internal/site/registry_test.go
T
2026-08-15 07:13:00 +00:00

150 lines
4.0 KiB
Go

package site
import (
"context"
"errors"
"sync"
"testing"
"github.com/iceBear67/simplepages/internal/store"
)
func TestRegistryLookupAndDelete(t *testing.T) {
r := NewRegistry()
if _, ok := r.Lookup("demo"); ok {
t.Fatal("an empty registry answered a lookup")
}
if r.Len() != 0 {
t.Fatalf("Len = %d on an empty registry", r.Len())
}
sp := r.Put(&store.Project{ID: 7, Name: "demo", IndexFile: "index.html"})
got, ok := r.Lookup("demo")
if !ok || got != sp {
t.Fatal("Put did not publish the project it returned")
}
if got.ID != 7 || got.Name != "demo" {
t.Errorf("entry = %+v", got)
}
if r.Len() != 1 {
t.Errorf("Len = %d, want 1", r.Len())
}
r.Delete("demo")
if _, ok := r.Lookup("demo"); ok {
t.Error("a deleted project still resolves")
}
r.Delete("demo") // deleting twice is not an error
r.Delete("never-existed")
}
// A settings change must not disturb what the project is currently serving: the
// entry is updated in place rather than rebuilt, so the active pointer survives.
func TestRegistryPutKeepsTheActiveDeployment(t *testing.T) {
r := NewRegistry()
p := &store.Project{ID: 1, Name: "demo", IndexFile: "index.html"}
sp := r.Put(p)
d := fixture("index.html")
sp.Activate(d)
p2 := &store.Project{ID: 1, Name: "demo", IndexFile: "main.html", SPAFallback: true}
again := r.Put(p2)
if again != sp {
t.Fatal("Put replaced the live entry instead of updating it")
}
if again.Active() != d {
t.Fatal("updating the settings dropped the active deployment")
}
if cfg := again.Config(); cfg.IndexFile != "main.html" || !cfg.SPAFallback {
t.Errorf("config = %+v, want the new settings", cfg)
}
}
// A project recreated under the same name is a different project, so its entry
// must start empty rather than inherit the old one's deployment.
func TestRegistryPutReplacesAnEntryWithADifferentID(t *testing.T) {
r := NewRegistry()
sp := r.Put(&store.Project{ID: 1, Name: "demo"})
sp.Activate(fixture("index.html"))
again := r.Put(&store.Project{ID: 2, Name: "demo"})
if again == sp {
t.Fatal("a different project id reused the old entry")
}
if again.Active() != nil {
t.Error("the new project inherited the old project's deployment")
}
}
func TestRegistryReplace(t *testing.T) {
r := NewRegistry()
r.Put(&store.Project{ID: 1, Name: "gone"})
r.Replace([]*Project{
NewProject(&store.Project{ID: 2, Name: "a"}),
NewProject(&store.Project{ID: 3, Name: "b"}),
})
if _, ok := r.Lookup("gone"); ok {
t.Error("Replace kept a project that is not in the new set")
}
if r.Len() != 2 {
t.Fatalf("Len = %d, want 2", r.Len())
}
names := map[string]bool{}
for _, p := range r.Projects() {
names[p.Name] = true
}
if !names["a"] || !names["b"] {
t.Errorf("Projects = %v", names)
}
}
// The API's ownership guard compares resolved ids, so this is the lookup that
// decides whether one project's key can touch another's deployment.
func TestRegistryResolveProject(t *testing.T) {
r := NewRegistry()
r.Put(&store.Project{ID: 42, Name: "demo"})
id, err := r.ResolveProject(context.Background(), "demo")
if err != nil || id != 42 {
t.Fatalf("ResolveProject = (%d, %v), want (42, nil)", id, err)
}
if _, err := r.ResolveProject(context.Background(), "other"); !errors.Is(err, store.ErrNotFound) {
t.Fatalf("ResolveProject on an unknown name = %v, want ErrNotFound", err)
}
}
// Readers take no lock at all, so this is worth running under -race: a writer
// mutating the map in place instead of cloning it would show up here.
func TestRegistryConcurrentReadersAndWriters(t *testing.T) {
r := NewRegistry()
r.Put(&store.Project{ID: 1, Name: "stable"})
stop := make(chan struct{})
var wg sync.WaitGroup
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
}
if p, ok := r.Lookup("stable"); !ok || p.ID != 1 {
t.Error("the stable project vanished while other projects changed")
return
}
r.Lookup("churn")
r.Len()
}
}()
}
for i := range 500 {
r.Put(&store.Project{ID: int64(i + 2), Name: "churn"})
r.Delete("churn")
}
close(stop)
wg.Wait()
}