| 1 | package docs_test |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/carlosframework/docs/internal/docs" |
| 10 | ) |
| 11 | |
| 12 | // newSampleDocumentsTestApp builds a development App exactly like |
| 13 | // docs.NewForTest, except with Config.SampleDocuments set explicitly |
| 14 | // — the same "build directly via New, package-internal test helper" |
| 15 | // shape auth_allowlist_test.go's newAllowlistedTestApp uses for the |
| 16 | // same reason: NewForTest itself never sets this, so any test that |
| 17 | // wants seeding on (or explicitly off) has to build its own App. |
| 18 | func newSampleDocumentsTestApp(t *testing.T, sampleDocuments string) *docs.App { |
| 19 | t.Helper() |
| 20 | app, err := docs.New(docs.Config{ |
| 21 | DBPath: filepath.Join(t.TempDir(), "docs.db"), |
| 22 | Env: "development", |
| 23 | SampleDocuments: sampleDocuments, |
| 24 | }) |
| 25 | if err != nil { |
| 26 | t.Fatalf("New: %v", err) |
| 27 | } |
| 28 | t.Cleanup(func() { |
| 29 | if cerr := app.Close(); cerr != nil { |
| 30 | t.Errorf("App.Close: %v", cerr) |
| 31 | } |
| 32 | }) |
| 33 | return app |
| 34 | } |
| 35 | |
| 36 | // TestNewPersonWithSampleDocumentsOnGetsExactlySix pins the design |
| 37 | // this branch exists to ship: a brand-new person's very first |
| 38 | // EnsurePerson call, on an instance that opted in via |
| 39 | // $DOCS_SAMPLE_DOCUMENTS, gets seed.go's own sample set — exactly six |
| 40 | // documents (len(seedDocs) in seed.go), owned by them, each with real, |
| 41 | // substantial content read back through the actual CRDT, never a row |
| 42 | // count standing in for content that was never applied. |
| 43 | // |
| 44 | // Read through Store.HTML, not Store.Text: the seed generator now |
| 45 | // builds a Y.XmlFragment on root "prosemirror" (fixtures/generate-seed.mjs), |
| 46 | // so a seeded document's Store.Text legitimately replays to "" — HTML |
| 47 | // is the read path that actually replays what Seed wrote. |
| 48 | func TestNewPersonWithSampleDocumentsOnGetsExactlySix(t *testing.T) { |
| 49 | app := newSampleDocumentsTestApp(t, "1") |
| 50 | ctx := context.Background() |
| 51 | |
| 52 | owner, err := app.Store().EnsurePerson(ctx, "brand-new@example.com") |
| 53 | if err != nil { |
| 54 | t.Fatalf("EnsurePerson: %v", err) |
| 55 | } |
| 56 | |
| 57 | list, err := app.Store().List(ctx, owner) |
| 58 | if err != nil { |
| 59 | t.Fatalf("List: %v", err) |
| 60 | } |
| 61 | if len(list) != 6 { |
| 62 | t.Fatalf("got %d documents, want exactly 6", len(list)) |
| 63 | } |
| 64 | |
| 65 | for _, d := range list { |
| 66 | html, err := app.Store().HTML(ctx, d) |
| 67 | if err != nil { |
| 68 | t.Fatalf("HTML(%q): %v", d.Title, err) |
| 69 | } |
| 70 | if len(strings.TrimSpace(string(html))) < 200 { |
| 71 | t.Errorf("%q has %d characters of HTML — want substantial, real content", d.Title, len(html)) |
| 72 | } |
| 73 | if d.OwnerID != owner { |
| 74 | t.Errorf("%q has OwnerID %d, want %d", d.Title, d.OwnerID, owner) |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | // TestNewPersonWithSampleDocumentsOffGetsNone is the opt-in's default: |
| 80 | // with $DOCS_SAMPLE_DOCUMENTS unset (SampleDocuments left "" here, |
| 81 | // exactly what a real deployment gets if nobody has set the variable |
| 82 | // at all), a brand-new person's list starts empty — the pre-existing, |
| 83 | // unchanged behaviour every real team instance must keep. |
| 84 | func TestNewPersonWithSampleDocumentsOffGetsNone(t *testing.T) { |
| 85 | app := newSampleDocumentsTestApp(t, "") |
| 86 | ctx := context.Background() |
| 87 | |
| 88 | owner, err := app.Store().EnsurePerson(ctx, "brand-new@example.com") |
| 89 | if err != nil { |
| 90 | t.Fatalf("EnsurePerson: %v", err) |
| 91 | } |
| 92 | |
| 93 | list, err := app.Store().List(ctx, owner) |
| 94 | if err != nil { |
| 95 | t.Fatalf("List: %v", err) |
| 96 | } |
| 97 | if len(list) != 0 { |
| 98 | t.Fatalf("got %d documents with SampleDocuments off, want 0", len(list)) |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // TestExistingPersonSigningInAgainGetsNoNewDocuments is the seam this |
| 103 | // whole design hangs off: EnsurePerson's create branch seeds, its |
| 104 | // found branch (Person row already exists) never does. Calling |
| 105 | // EnsurePerson a second time for the same address must not grow the |
| 106 | // document count at all, whether or not sample content already |
| 107 | // landed on the first call. |
| 108 | func TestExistingPersonSigningInAgainGetsNoNewDocuments(t *testing.T) { |
| 109 | app := newSampleDocumentsTestApp(t, "true") |
| 110 | ctx := context.Background() |
| 111 | |
| 112 | owner, err := app.Store().EnsurePerson(ctx, "returning@example.com") |
| 113 | if err != nil { |
| 114 | t.Fatalf("EnsurePerson (first): %v", err) |
| 115 | } |
| 116 | first, err := app.Store().List(ctx, owner) |
| 117 | if err != nil { |
| 118 | t.Fatalf("List (first): %v", err) |
| 119 | } |
| 120 | if len(first) != 6 { |
| 121 | t.Fatalf("got %d documents after first sign-in, want 6", len(first)) |
| 122 | } |
| 123 | |
| 124 | ownerAgain, err := app.Store().EnsurePerson(ctx, "returning@example.com") |
| 125 | if err != nil { |
| 126 | t.Fatalf("EnsurePerson (second): %v", err) |
| 127 | } |
| 128 | if ownerAgain != owner { |
| 129 | t.Fatalf("EnsurePerson(again) = %d, want the same person id %d", ownerAgain, owner) |
| 130 | } |
| 131 | |
| 132 | second, err := app.Store().List(ctx, owner) |
| 133 | if err != nil { |
| 134 | t.Fatalf("List (second): %v", err) |
| 135 | } |
| 136 | if len(second) != len(first) { |
| 137 | t.Fatalf("document count grew from %d to %d on a repeat sign-in", len(first), len(second)) |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // TestTwoPeopleEachGetTheirOwnSampleSet checks seeding does not |
| 142 | // undermine owned() (store.go): two different brand-new people each |
| 143 | // get their own full set of six, and List for one never includes a |
| 144 | // row belonging to the other — the same ownership guarantee |
| 145 | // TestOwnedScopesByOwner (scope_test.go) pins for hand-created |
| 146 | // documents, exercised here through the seeding path instead. |
| 147 | func TestTwoPeopleEachGetTheirOwnSampleSet(t *testing.T) { |
| 148 | app := newSampleDocumentsTestApp(t, "yes") |
| 149 | ctx := context.Background() |
| 150 | |
| 151 | alice, err := app.Store().EnsurePerson(ctx, "alice-sample@example.com") |
| 152 | if err != nil { |
| 153 | t.Fatalf("EnsurePerson(alice): %v", err) |
| 154 | } |
| 155 | bob, err := app.Store().EnsurePerson(ctx, "bob-sample@example.com") |
| 156 | if err != nil { |
| 157 | t.Fatalf("EnsurePerson(bob): %v", err) |
| 158 | } |
| 159 | if alice == bob { |
| 160 | t.Fatalf("alice and bob resolved to the same person id %d", alice) |
| 161 | } |
| 162 | |
| 163 | aliceDocs, err := app.Store().List(ctx, alice) |
| 164 | if err != nil { |
| 165 | t.Fatalf("List(alice): %v", err) |
| 166 | } |
| 167 | bobDocs, err := app.Store().List(ctx, bob) |
| 168 | if err != nil { |
| 169 | t.Fatalf("List(bob): %v", err) |
| 170 | } |
| 171 | if len(aliceDocs) != 6 { |
| 172 | t.Fatalf("alice has %d documents, want 6", len(aliceDocs)) |
| 173 | } |
| 174 | if len(bobDocs) != 6 { |
| 175 | t.Fatalf("bob has %d documents, want 6", len(bobDocs)) |
| 176 | } |
| 177 | |
| 178 | for _, d := range aliceDocs { |
| 179 | if d.OwnerID != alice { |
| 180 | t.Errorf("alice's List returned a document owned by %d, not %d", d.OwnerID, alice) |
| 181 | } |
| 182 | if _, err := app.Store().Get(ctx, bob, d.Sqid); err == nil { |
| 183 | t.Errorf("bob can Get alice's document %q — owned() is not isolating seeded documents", d.Sqid) |
| 184 | } |
| 185 | } |
| 186 | for _, d := range bobDocs { |
| 187 | if d.OwnerID != bob { |
| 188 | t.Errorf("bob's List returned a document owned by %d, not %d", d.OwnerID, bob) |
| 189 | } |
| 190 | if _, err := app.Store().Get(ctx, alice, d.Sqid); err == nil { |
| 191 | t.Errorf("alice can Get bob's document %q — owned() is not isolating seeded documents", d.Sqid) |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | |