1package daemon23import (4 "context"5 "strings"6 "testing"78 "github.com/FelineStateMachine/tinyrelay/internal/catalog"9 "github.com/FelineStateMachine/tinyrelay/internal/storage"10)1112const creationOwner = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"1314func TestAfterCreateSeedsRecurringSourceBeforeReady(t *testing.T) {15 ctx := context.Background()16 app, err := New(ctx, Config{DataDir: t.TempDir(), DefaultTenant: "main"})17 if err != nil {18 t.Fatal(err)19 }20 defer app.Close(ctx)21 meta, err := app.Catalog().Create(ctx, catalog.CreateOptions{Name: "search", Owner: creationOwner, Template: "search", Source: "wss://source.example"})22 if err != nil {23 t.Fatal(err)24 }25 if err := app.AfterCreate(ctx, meta, CreateOptions{}); err != nil {26 t.Fatal(err)27 }28 var state, payload string29 store, err := openCreationStore(ctx, meta)30 if err != nil {31 t.Fatal(err)32 }33 defer store.Close()34 if err := store.DB().QueryRow("SELECT state,payload FROM replication_jobs WHERE id=?", "source-"+meta.ID).Scan(&state, &payload); err != nil {35 t.Fatal(err)36 }37 if state != "pending" || payload == "" {38 t.Fatalf("source job = %q %q", state, payload)39 }40 if err := app.Catalog().MarkReady(ctx, meta.ID); err != nil {41 t.Fatal(err)42 }43}4445func TestAfterCreateRequiresSearchSourceAndRecoveryLeavesItCreating(t *testing.T) {46 ctx := context.Background()47 app, err := New(ctx, Config{DataDir: t.TempDir(), DefaultTenant: "main"})48 if err != nil {49 t.Fatal(err)50 }51 defer app.Close(ctx)52 meta, err := app.Catalog().Create(ctx, catalog.CreateOptions{Name: "search", Owner: creationOwner, Template: "search"})53 if err != nil {54 t.Fatal(err)55 }56 if err := app.RecoverCreating(ctx); err == nil {57 t.Fatal("recovery accepted missing required source")58 }59 got, err := app.Catalog().GetByID(ctx, meta.ID)60 if err != nil {61 t.Fatal(err)62 }63 if got.Status != catalog.StatusCreating {64 t.Fatalf("status = %q, want creating", got.Status)65 }66}6768func TestInboxSourceFollowsOwnerWithHourlyPull(t *testing.T) {69 ctx := context.Background()70 app, err := New(ctx, Config{DataDir: t.TempDir(), DefaultTenant: "main"})71 if err != nil {72 t.Fatal(err)73 }74 defer app.Close(ctx)75 meta, err := app.Catalog().Create(ctx, catalog.CreateOptions{Name: "inbox", Owner: creationOwner, Template: "inbox", Source: "https://upstream.example/relay"})76 if err != nil {77 t.Fatal(err)78 }79 if err := app.AfterCreate(ctx, meta, CreateOptions{}); err != nil {80 t.Fatal(err)81 }82 store, err := openCreationStore(ctx, meta)83 if err != nil {84 t.Fatal(err)85 }86 defer store.Close()87 var payload string88 if err := store.DB().QueryRow("SELECT payload FROM replication_jobs WHERE id=?", "source-"+meta.ID).Scan(&payload); err != nil {89 t.Fatal(err)90 }91 if !strings.Contains(payload, `"every":1`) || !strings.Contains(payload, "#p") || !strings.Contains(payload, creationOwner) {92 t.Fatalf("inbox source job payload = %s", payload)93 }94 if strings.Contains(payload, "https://") || !strings.Contains(payload, "wss://upstream.example/relay") {95 t.Fatalf("inbox source URL was not converted: %s", payload)96 }97}9899func TestInboxWithoutSourceSeedsDormantOwnerReadRelayDiscovery(t *testing.T) {100 ctx := context.Background()101 app, err := New(ctx, Config{DataDir: t.TempDir(), DefaultTenant: "main"})102 if err != nil {103 t.Fatal(err)104 }105 defer app.Close(ctx)106 meta, err := app.Catalog().Create(ctx, catalog.CreateOptions{Name: "inbox", Owner: creationOwner, Template: "inbox"})107 if err != nil {108 t.Fatal(err)109 }110 if err := app.AfterCreate(ctx, meta, CreateOptions{}); err != nil {111 t.Fatal(err)112 }113 store, err := openCreationStore(ctx, meta)114 if err != nil {115 t.Fatal(err)116 }117 defer store.Close()118 var payload string119 if err := store.DB().QueryRow("SELECT payload FROM replication_jobs WHERE id=?", "source-"+meta.ID).Scan(&payload); err != nil {120 t.Fatal(err)121 }122 if !strings.Contains(payload, `"discoverPubKey":"`+creationOwner+`"`) || !strings.Contains(payload, `"every":1`) || strings.Contains(payload, `"relays"`) {123 t.Fatalf("inbox discovery job payload = %s", payload)124 }125}126127func TestInboxWithoutSourceJobUsesRuntimeDiscoverySeeds(t *testing.T) {128 ctx := context.Background()129 app, err := New(ctx, Config{DataDir: t.TempDir(), DefaultTenant: "main"})130 if err != nil {131 t.Fatal(err)132 }133 defer app.Close(ctx)134 meta, err := app.Catalog().Create(ctx, catalog.CreateOptions{Name: "inbox", Owner: creationOwner, Template: "inbox"})135 if err != nil {136 t.Fatal(err)137 }138 if err := app.AfterCreate(ctx, meta, CreateOptions{}); err != nil {139 t.Fatal(err)140 }141 store, err := openCreationStore(ctx, meta)142 if err != nil {143 t.Fatal(err)144 }145 defer store.Close()146 var state, payload string147 if err := store.DB().QueryRow("SELECT state,payload FROM replication_jobs WHERE id=?", "source-"+meta.ID).Scan(&state, &payload); err != nil {148 t.Fatal(err)149 }150 if state != "pending" || !strings.Contains(payload, `"discoverPubKey":"`+creationOwner+`"`) {151 t.Fatalf("runtime discovery job = %q %s", state, payload)152 }153}154155func openCreationStore(ctx context.Context, meta catalog.Tenant) (*storage.Store, error) {156 return storage.Open(ctx, meta.Paths.Database)157}