clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / internal / catalog / catalog_test.go
catalog_test.go 6.5K
1package catalog23import (4 "context"5 "errors"6 "path/filepath"7 "strings"8 "sync"9 "testing"10)1112const testOwner = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"1314func TestCreateAndLifecycle(t *testing.T) {15 cat := openTestCatalog(t)16 tenant, err := cat.Create(context.Background(), CreateOptions{Name: "Example", Owner: testOwner, Template: "public"})17 if err != nil {18 t.Fatal(err)19 }20 if tenant.Status != StatusCreating || tenant.Owner != testOwner {21 t.Fatalf("unexpected tenant: %+v", tenant)22 }23 if tenant.Paths.Database != filepath.Join(tenant.Paths.Root, "relay.db") {24 t.Fatalf("database path is not stable: %+v", tenant.Paths)25 }26 if tenant.Paths.Blobs == tenant.Paths.Git || !strings.HasSuffix(tenant.Paths.Blobs, "blobs") {27 t.Fatalf("unexpected tenant paths: %+v", tenant.Paths)28 }29 if err := cat.MarkReady(context.Background(), tenant.ID); err != nil {30 t.Fatal(err)31 }32 ready, err := cat.GetByID(context.Background(), tenant.ID)33 if err != nil {34 t.Fatal(err)35 }36 if ready.Status != StatusReady {37 t.Fatalf("status = %q, want ready", ready.Status)38 }39 if err := cat.Disable(context.Background(), tenant.ID); err != nil {40 t.Fatal(err)41 }42 if err := cat.Enable(context.Background(), tenant.ID); err != nil {43 t.Fatal(err)44 }45 newOwner := "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"46 if err := cat.TransferOwner(context.Background(), tenant.ID, testOwner, newOwner); err != nil {47 t.Fatal(err)48 }49 updated, err := cat.GetByID(context.Background(), tenant.ID)50 if err != nil || updated.Owner != newOwner {51 t.Fatalf("owner after transfer = %q, err=%v", updated.Owner, err)52 }53}5455func TestSQLiteDurabilityPragmas(t *testing.T) {56 cat := openTestCatalog(t)57 var synchronous string58 if err := cat.db.QueryRow("PRAGMA synchronous").Scan(&synchronous); err != nil {59 t.Fatal(err)60 }61 if synchronous != "2" {62 t.Fatalf("synchronous = %q, want FULL (2)", synchronous)63 }64}6566func TestCreateRejectsInvalidOwnerAndUnsafeName(t *testing.T) {67 cat := openTestCatalog(t)68 for _, tc := range []struct {69 name string70 owner string71 }{72 {"ok", "bad"},73 {"../escape", testOwner},74 {"", testOwner},75 } {76 _, err := cat.Create(context.Background(), CreateOptions{Name: tc.name, Owner: tc.owner})77 if err == nil {78 t.Errorf("Create(%q, %q) succeeded", tc.name, tc.owner)79 }80 }81}8283func TestNameAndHostUniquenessAndNormalization(t *testing.T) {84 cat := openTestCatalog(t)85 one, err := cat.Create(context.Background(), CreateOptions{Name: "First", Owner: testOwner})86 if err != nil {87 t.Fatal(err)88 }89 if _, err := cat.Create(context.Background(), CreateOptions{Name: " first ", Owner: testOwner}); !errors.Is(err, ErrNameTaken) {90 t.Fatalf("duplicate name error = %v, want ErrNameTaken", err)91 }92 if err := cat.SetHost(context.Background(), one.ID, "Relay.Example.COM."); err != nil {93 t.Fatal(err)94 }95 resolved, err := cat.ResolveHost(context.Background(), "relay.example.com")96 if err != nil {97 t.Fatal(err)98 }99 if resolved.ID != one.ID {100 t.Fatalf("resolved %q, want %q", resolved.ID, one.ID)101 }102 two, err := cat.Create(context.Background(), CreateOptions{Name: "Second", Owner: testOwner})103 if err != nil {104 t.Fatal(err)105 }106 if err := cat.SetHost(context.Background(), two.ID, "relay.example.com"); !errors.Is(err, ErrHostTaken) {107 t.Fatalf("duplicate host error = %v, want ErrHostTaken", err)108 }109 if err := cat.ClearHost(context.Background(), one.ID); err != nil {110 t.Fatal(err)111 }112 if _, err := cat.ResolveHost(context.Background(), "relay.example.com"); !errors.Is(err, ErrNotFound) {113 t.Fatalf("cleared host error = %v, want ErrNotFound", err)114 }115}116117func TestHostMappingsResolveAndRemainUnique(t *testing.T) {118 cat := openTestCatalog(t)119 one, err := cat.Create(context.Background(), CreateOptions{Name: "First", Owner: testOwner})120 if err != nil {121 t.Fatal(err)122 }123 two, err := cat.Create(context.Background(), CreateOptions{Name: "Second", Owner: testOwner})124 if err != nil {125 t.Fatal(err)126 }127 mapping, err := cat.AddHost(context.Background(), one.ID, "Relay.Alice.Example.", "site-label")128 if err != nil {129 t.Fatal(err)130 }131 if mapping.Host != "relay.alice.example" || mapping.Site != "site-label" {132 t.Fatalf("mapping = %+v", mapping)133 }134 resolved, err := cat.ResolveHost(context.Background(), "relay.alice.example")135 if err != nil || resolved.ID != one.ID {136 t.Fatalf("resolved = %+v, err=%v", resolved, err)137 }138 if _, err := cat.AddHost(context.Background(), two.ID, "relay.alice.example", ""); !errors.Is(err, ErrHostTaken) {139 t.Fatalf("duplicate alias error = %v", err)140 }141 if err := cat.SetHost(context.Background(), two.ID, "relay.alice.example"); !errors.Is(err, ErrHostTaken) {142 t.Fatalf("primary/alias collision = %v", err)143 }144 updated, err := cat.SetHostSite(context.Background(), one.ID, "relay.alice.example", "other")145 if err != nil || updated.Site != "other" {146 t.Fatalf("updated = %+v, err=%v", updated, err)147 }148 if err := cat.RemoveHost(context.Background(), one.ID, "relay.alice.example"); err != nil {149 t.Fatal(err)150 }151 if _, err := cat.ResolveHostMapping(context.Background(), "relay.alice.example"); !errors.Is(err, ErrHostMappingNotFound) {152 t.Fatalf("removed alias error = %v", err)153 }154}155156func TestConcurrentCreatesDoNotDuplicateNames(t *testing.T) {157 cat := openTestCatalog(t)158 const workers = 12159 var wg sync.WaitGroup160 results := make(chan error, workers)161 for range workers {162 wg.Add(1)163 go func() {164 defer wg.Done()165 _, err := cat.Create(context.Background(), CreateOptions{Name: "same", Owner: testOwner})166 results <- err167 }()168 }169 wg.Wait()170 close(results)171 created := 0172 for err := range results {173 if err == nil {174 created++175 } else if !errors.Is(err, ErrNameTaken) {176 t.Errorf("unexpected concurrent create error: %v", err)177 }178 }179 if created != 1 {180 t.Fatalf("created %d tenants, want 1", created)181 }182}183184func TestRestartKeepsCreatingTenant(t *testing.T) {185 root := t.TempDir()186 one, err := Open(context.Background(), root)187 if err != nil {188 t.Fatal(err)189 }190 tenant, err := one.Create(context.Background(), CreateOptions{Name: "recover", Owner: testOwner})191 if err != nil {192 t.Fatal(err)193 }194 if err := one.Close(); err != nil {195 t.Fatal(err)196 }197 two, err := Open(context.Background(), root)198 if err != nil {199 t.Fatal(err)200 }201 recovered, err := two.GetByID(context.Background(), tenant.ID)202 if err != nil {203 t.Fatal(err)204 }205 if recovered.Status != StatusCreating {206 t.Fatalf("status after restart = %q", recovered.Status)207 }208}209210func openTestCatalog(t *testing.T) *Catalog {211 t.Helper()212 cat, err := Open(context.Background(), t.TempDir())213 if err != nil {214 t.Fatal(err)215 }216 t.Cleanup(func() { _ = cat.Close() })217 return cat218}