592 lines
16 KiB
Go
592 lines
16 KiB
Go
|
|
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+`"`)
|
||
|
|
}
|