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