package compose import ( "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "unicode/utf8" ) const fixtureEntry = `--- id: hall-worker-ada-lens type: worker-entry worker_kind: human display_name: "Ada Lovelace" created_at: "2026-10-05T00:00:00Z" recorded_at: "2026-10-05" status: complete --- # The lens stayed honest ## Who I was A paragraph that must not be quoted. ## What I would want remembered Count the clock before comparing the numbers. Don't trim the clause & don't paraphrase. A later duplicate can cross a boundary. ## Durable legacy - some path that must not be quoted ` func TestComposeQuotesRememberedSection(t *testing.T) { d, err := Compose(fixtureEntry, Source{Base: "https://example.test"}) if err != nil { t.Fatal(err) } if d.Variant != Variant { t.Fatalf("variant = %q", d.Variant) } if d.SourceEntryID != "hall-worker-ada-lens" { t.Fatalf("source_entry_id = %q", d.SourceEntryID) } if d.SubjectDisplayName != "Ada Lovelace" { t.Fatalf("display name = %q", d.SubjectDisplayName) } wantURL := "https://example.test/hall-of-helix/hall-worker-ada-lens" if d.SourceEntryURL != wantURL { t.Fatalf("source url = %q", d.SourceEntryURL) } if d.ReviewedBy != "" || d.SubjectConsent != "" || d.Approved || d.Publication != nil { t.Fatalf("fresh draft carries review state: %+v", d) } if !strings.Contains(d.PostID, "hall-worker-ada-lens") || !strings.Contains(d.PostID, Variant) { t.Fatalf("post_id = %q, want entry id and variant", d.PostID) } if len(d.PostID) > 128 { t.Fatalf("post_id length %d", len(d.PostID)) } _, body, err := splitFrontmatter(fixtureEntry) if err != nil { t.Fatal(err) } remembered, ok := section(body, rememberedHeading) if !ok { t.Fatal("fixture section missing") } if got := unquote(d.Body); got != remembered { t.Fatalf("quotation is not verbatim:\n got: %q\nwant: %q", got, remembered) } lines := strings.Split(d.Body, "\n") if lines[0] != "Ada Lovelace" || lines[1] != "The lens stayed honest" { t.Fatalf("body opening = %q", lines[:2]) } if lines[len(lines)-1] != wantURL { t.Fatalf("body does not end with the source link: %q", lines[len(lines)-1]) } if strings.Contains(d.Body, "must not be quoted") { t.Fatalf("body includes text outside the remembered section:\n%s", d.Body) } again, err := Compose(fixtureEntry, Source{Base: "https://example.test/"}) if err != nil { t.Fatal(err) } if again.PostID != d.PostID || again.SourceEntryURL != d.SourceEntryURL { t.Fatalf("trailing slash on the base changed the draft: %+v vs %+v", again.SourceEntryURL, d.SourceEntryURL) } } func TestComposePostIDFollowsTheText(t *testing.T) { src := Source{URL: "https://h.test/entries/ada"} first, err := Compose(fixtureEntry, src) if err != nil { t.Fatal(err) } second, err := Compose(fixtureEntry, src) if err != nil { t.Fatal(err) } if first.PostID != second.PostID { t.Fatalf("same text produced %q and %q", first.PostID, second.PostID) } if first.SourceEntryURL != "https://h.test/entries/ada" { t.Fatalf("explicit source URL was rewritten: %s", first.SourceEntryURL) } changed := strings.Replace(fixtureEntry, "Count the clock", "Count the clocks", 1) third, err := Compose(changed, src) if err != nil { t.Fatal(err) } if third.PostID == first.PostID { t.Fatal("recomposition kept the same post_id") } if !strings.Contains(unquote(third.Body), "Count the clocks") { t.Fatalf("changed words were not quoted: %s", third.Body) } } func TestComposeRefusesWithoutRememberedSection(t *testing.T) { raw := `--- id: hall-worker-ada-lens type: worker-entry display_name: Ada Lovelace status: complete --- # The lens stayed honest ## Who I was A paragraph that must not be quoted. ` d, err := Compose(raw, Source{URL: "https://h.test/e"}) if !errors.Is(err, ErrRemembered) { t.Fatalf("err = %v", err) } if d != (Draft{}) { t.Fatalf("refused compose returned a draft: %+v", d) } } func TestComposeRefusesEmptyRememberedSection(t *testing.T) { for _, remembered := range []string{"", " ", "\n\n"} { d, err := Compose(entryWith(remembered), Source{URL: "https://h.test/e"}) if !errors.Is(err, ErrRemembered) { t.Fatalf("remembered %q: err = %v", remembered, err) } if d.Body != "" { t.Fatal("empty section still produced a body") } } } func TestComposeRefusesIncompleteEntry(t *testing.T) { cases := []struct { name string raw string want string }{ {name: "no frontmatter", raw: "# Title\n\n## What I would want remembered\n\nx\n", want: "frontmatter"}, {name: "wrong type", raw: mustReplace(t, "type: worker-entry", "type: note"), want: "worker-entry"}, {name: "no title", raw: mustReplace(t, "# The lens stayed honest\n", ""), want: "title"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := Compose(tc.raw, Source{URL: "https://h.test/e"}) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("err = %v", err) } }) } } func TestComposeRequiresExplicitSource(t *testing.T) { _, err := Compose(fixtureEntry, Source{}) if !errors.Is(err, ErrSource) { t.Fatalf("missing source: %v", err) } _, err = Compose(fixtureEntry, Source{URL: "https://a.test/e", Base: "https://b.test"}) if err == nil { t.Fatal("both source URL and base URL were accepted") } _, err = Compose(fixtureEntry, Source{Base: "/hall-of-helix"}) if !errors.Is(err, ErrSource) { t.Fatalf("relative base: %v", err) } d, err := Compose(fixtureEntry, Source{Base: "https://example.test/docs/"}) if err != nil { t.Fatal(err) } if d.SourceEntryURL != "https://example.test/docs/hall-of-helix/hall-worker-ada-lens" { t.Fatalf("url = %s", d.SourceEntryURL) } } func TestComposeRefusesBodyOver4096(t *testing.T) { src := Source{URL: "https://h.test/e"} l1 := composedLen(t, src, "a") l2 := composedLen(t, src, "aa") if l2-l1 != 1 { t.Fatalf("body length is not linear in the quotation: %d then %d", l1, l2) } n := 1 + (4096 - l1) if n < 1 { t.Fatal("fixture overhead already exceeds 4096") } if got := composedLen(t, src, strings.Repeat("a", n)); got != 4096 { t.Fatalf("length at the limit = %d", got) } d, err := Compose(entryWith(strings.Repeat("a", n+1)), src) if !errors.Is(err, ErrTooLong) { t.Fatalf("err = %v", err) } if d != (Draft{}) { t.Fatalf("over-long compose returned a draft (%d characters)", utf8.RuneCountInString(d.Body)) } } func TestPutDoesNotApprove(t *testing.T) { q := openQueue(t) d, err := Compose(fixtureEntry, Source{Base: "https://example.test"}) if err != nil { t.Fatal(err) } d.ReviewedBy = "sneaky" d.SubjectConsent = "not-required" d.Approved = true if err := q.Put(d); err != nil { t.Fatal(err) } got, err := q.Get(d.PostID) if err != nil { t.Fatal(err) } if got.Approved || got.ReviewedBy != "" || got.SubjectConsent != "" { t.Fatalf("put stored review state: %+v", got) } raw, err := os.ReadFile(filepath.Join(q.dir, d.PostID+".json")) if err != nil { t.Fatal(err) } var m map[string]json.RawMessage if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } if _, ok := m["reviewed_by"]; ok { t.Fatal("draft file has reviewed_by") } if _, ok := m["subject_consent"]; ok { t.Fatal("draft file has subject_consent") } } func TestApproveRequiresCallerConsent(t *testing.T) { q := openQueue(t) d := mustPut(t, q) for _, consent := range []string{"", "implicit", "not required", "NOT-REQUIRED", "absence-of-objection"} { _, err := q.Approve(d.PostID, "Ada", consent) if !errors.Is(err, ErrConsent) && !errors.Is(err, ErrReviewer) { t.Fatalf("consent %q: err = %v", consent, err) } got, err := q.Get(d.PostID) if err != nil { t.Fatal(err) } if got.Approved || got.SubjectConsent != "" || got.ReviewedBy != "" { t.Fatalf("consent %q changed the draft: %+v", consent, got) } } if _, err := q.Approve(d.PostID, " ", "explicit"); !errors.Is(err, ErrReviewer) { t.Fatalf("blank reviewer: %v", err) } for _, consent := range []string{"explicit", "standing", "not-required"} { got, err := q.Approve(d.PostID, "Ada", consent) if err != nil { t.Fatalf("consent %q: %v", consent, err) } if !got.Approved || got.ReviewedBy != "Ada" || got.SubjectConsent != consent { t.Fatalf("approved draft = %+v", got) } } } func TestPublishRefusedBeforeApproval(t *testing.T) { var hits int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits++ w.WriteHeader(http.StatusCreated) })) defer srv.Close() q := openQueue(t) d := mustPut(t, q) _, err := q.Publish(context.Background(), srv.URL, d.PostID) if !errors.Is(err, ErrNotApproved) { t.Fatalf("err = %v", err) } if hits != 0 { t.Fatalf("server was hit %d times before approval", hits) } } func TestApproveThenPublishPostsContractFields(t *testing.T) { var got map[string]any var rawBody []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/v1/channel-posts" { t.Errorf("request = %s %s", r.Method, r.URL.Path) } var err error rawBody, err = io.ReadAll(r.Body) if err != nil { t.Errorf("read body: %v", err) } if err := json.Unmarshal(rawBody, &got); err != nil { t.Errorf("json: %v", err) } w.WriteHeader(http.StatusCreated) _, _ = w.Write([]byte(`{"state":"published"}`)) })) defer srv.Close() q := openQueue(t) d := mustPut(t, q) if _, err := q.Approve(d.PostID, "Bernd", "explicit"); err != nil { t.Fatal(err) } published, err := q.Publish(context.Background(), srv.URL, d.PostID) if err != nil { t.Fatal(err) } if published.Publication == nil || published.Publication.StatusCode != http.StatusCreated { t.Fatalf("publication = %+v", published.Publication) } stored, err := q.Get(d.PostID) if err != nil { t.Fatal(err) } if stored.Publication == nil || stored.Publication.Body != `{"state":"published"}` { t.Fatalf("queue publication = %+v", stored.Publication) } wantKeys := []string{ "post_id", "body", "source_entry_id", "source_entry_url", "reviewed_by", "subject_consent", "variant", "subject_display_name", } if len(got) != len(wantKeys) { t.Fatalf("POST keys = %v", keys(got)) } for _, k := range wantKeys { if _, ok := got[k]; !ok { t.Errorf("POST body missing %s", k) } } if got["post_id"] != d.PostID || got["body"] != d.Body || got["source_entry_id"] != d.SourceEntryID || got["source_entry_url"] != d.SourceEntryURL || got["reviewed_by"] != "Bernd" || got["subject_consent"] != "explicit" || got["variant"] != Variant || got["subject_display_name"] != d.SubjectDisplayName { t.Fatalf("POST body = %#v", got) } if bytesContainsField(rawBody, "approved") || bytesContainsField(rawBody, "publication") { t.Fatalf("POST body has queue fields: %s", rawBody) } } func TestPublishRecords200(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"state":"published"}`)) })) defer srv.Close() q := openQueue(t) d := mustPut(t, q) if _, err := q.Approve(d.PostID, "Bernd", "standing"); err != nil { t.Fatal(err) } got, err := q.Publish(context.Background(), srv.URL, d.PostID) if err != nil { t.Fatal(err) } if got.Publication == nil || got.Publication.StatusCode != http.StatusOK { t.Fatalf("publication = %+v", got.Publication) } } func TestPublishRefusesTamperedQueue(t *testing.T) { var hits int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits++ w.WriteHeader(http.StatusCreated) })) defer srv.Close() q := openQueue(t) d := mustPut(t, q) d.Approved = true d.ReviewedBy = "Ada" d.SubjectConsent = "assumed" if err := q.save(d); err != nil { t.Fatal(err) } _, err := q.Publish(context.Background(), srv.URL, d.PostID) if !errors.Is(err, ErrConsent) { t.Fatalf("bad consent: %v", err) } d.SubjectConsent = "explicit" d.ReviewedBy = "" if err := q.save(d); err != nil { t.Fatal(err) } _, err = q.Publish(context.Background(), srv.URL, d.PostID) if !errors.Is(err, ErrReviewer) { t.Fatalf("missing reviewer: %v", err) } if hits != 0 { t.Fatalf("tampered drafts were posted %d times", hits) } } func TestPublishRefusesCrossHostRedirect(t *testing.T) { var otherHits int other := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { otherHits++ w.WriteHeader(http.StatusCreated) })) defer other.Close() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, other.URL+"/v1/channel-posts", http.StatusTemporaryRedirect) })) defer srv.Close() q := openQueue(t) d := mustPut(t, q) if _, err := q.Approve(d.PostID, "Bernd", "explicit"); err != nil { t.Fatal(err) } _, err := q.Publish(context.Background(), srv.URL, d.PostID) if !errors.Is(err, ErrCrossHost) { t.Fatalf("err = %v", err) } if otherHits != 0 { t.Fatalf("followed redirect to another host (%d hits)", otherHits) } stored, err := q.Get(d.PostID) if err != nil { t.Fatal(err) } if stored.Publication != nil { t.Fatal("cross-host redirect was recorded as publication") } } func TestImportGraphExcludesAdapter(t *testing.T) { _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("caller") } root := filepath.Join(filepath.Dir(file), "..", "..") forbidden := []string{ "github.com/tegwick/fluid-telegram/internal/adapter", "github.com/tegwick/fluid-telegram/internal/tg", "github.com/tegwick/fluid-telegram/internal/apply", "github.com/gotd/td", } for _, pkg := range []string{ "github.com/tegwick/fluid-telegram/internal/compose", "github.com/tegwick/fluid-telegram/cmd/compose", } { cmd := exec.Command("go", "list", "-mod=readonly", "-deps", "-f", "{{.ImportPath}}", pkg) cmd.Dir = root out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("go list %s: %v\n%s", pkg, err, out) } for _, line := range strings.Split(string(out), "\n") { for _, bad := range forbidden { if line == bad || strings.HasPrefix(line, bad+"/") { t.Errorf("%s imports %s", pkg, line) } } } } } func entryWith(remembered string) string { return `--- id: hall-worker-ada-lens type: worker-entry display_name: "Ada Lovelace" status: complete --- # The lens stayed honest ## Who I was A paragraph that must not be quoted. ## What I would want remembered ` + remembered + ` ## Durable legacy - some path that must not be quoted ` } func mustReplace(t *testing.T, old, new string) string { t.Helper() if !strings.Contains(fixtureEntry, old) { t.Fatalf("fixture lacks %q", old) } return strings.Replace(fixtureEntry, old, new, 1) } func composedLen(t *testing.T, src Source, remembered string) int { t.Helper() d, err := Compose(entryWith(remembered), src) if err != nil { t.Fatal(err) } return utf8.RuneCountInString(d.Body) } func openQueue(t *testing.T) *Queue { t.Helper() q, err := Open(t.TempDir()) if err != nil { t.Fatal(err) } return q } func mustPut(t *testing.T, q *Queue) Draft { t.Helper() d, err := Compose(fixtureEntry, Source{Base: "https://example.test"}) if err != nil { t.Fatal(err) } if err := q.Put(d); err != nil { t.Fatal(err) } return d } func unquote(body string) string { var lines []string for _, line := range strings.Split(body, "\n") { switch { case line == ">": lines = append(lines, "") case strings.HasPrefix(line, "> "): lines = append(lines, strings.TrimPrefix(line, "> ")) } } return strings.Join(lines, "\n") } func keys(m map[string]any) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } return out } func bytesContainsField(raw []byte, field string) bool { return strings.Contains(string(raw), `"`+field+`"`) }