package api_test import ( "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mathiasbq/hyperguild/ingestion/internal/api" ) // hyperguild#153: readers of brain/raw/ followed symlinks. /pending showed the // target's contents and promote would copy it into wiki/ (and brain-sync to // Gitea). raw/ is writable via Syncthing, so a symlink there is reachable // input. Every raw/ reader now treats a symlink as not-a-note. const sentinel153 = "SENTINEL-153-OUTSIDE" func rawWithSymlink(t *testing.T) (brain string) { t.Helper() brain = t.TempDir() raw := filepath.Join(brain, "raw") require.NoError(t, os.MkdirAll(raw, 0o755)) outside := filepath.Join(t.TempDir(), "secret.md") require.NoError(t, os.WriteFile(outside, []byte("---\ncreated_at: 2026-01-01T00:00:00Z\n---\n# s\n\n"+sentinel153), 0o644)) require.NoError(t, os.Symlink(outside, filepath.Join(raw, "2026-01-01-leak.md"))) require.NoError(t, os.WriteFile(filepath.Join(raw, "2026-01-02-real.md"), []byte("# real\n\nreal body"), 0o644)) return brain } func TestListPendingSkipsSymlinkedNotes(t *testing.T) { notes, err := api.ListPending(rawWithSymlink(t)) require.NoError(t, err) var names []string for _, n := range notes { names = append(names, n.Filename) assert.NotContains(t, n.Excerpt, sentinel153) } assert.Equal(t, []string{"2026-01-02-real.md"}, names) } func TestPromoteNoteRefusesASymlinkedSource(t *testing.T) { brain := rawWithSymlink(t) _, err := api.PromoteNote(brain, api.PromoteOptions{Filename: "2026-01-01-leak.md", Wing: "testing", Hall: "facts"}) require.Error(t, err) _, statErr := os.Stat(filepath.Join(brain, "wiki", "testing", "facts", "leak.md")) assert.True(t, os.IsNotExist(statErr), "nothing may be promoted from a symlink") } func TestRejectNoteRefusesASymlinkedSource(t *testing.T) { brain := rawWithSymlink(t) _, err := api.RejectNote(brain, api.RejectOptions{Filename: "2026-01-01-leak.md"}) require.Error(t, err) b, _ := os.ReadFile(filepath.Join(brain, "raw", "rejected", "2026-01-01-leak.md")) assert.False(t, strings.Contains(string(b), sentinel153)) } // Item 4: if raw/ itself is a symlink, containment inside it means nothing. func TestRawReadersRefuseASymlinkedRawRoot(t *testing.T) { brain := t.TempDir() elsewhere := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(elsewhere, "x.md"), []byte("# x\n\n"+sentinel153), 0o644)) require.NoError(t, os.Symlink(elsewhere, filepath.Join(brain, "raw"))) _, err := api.ResolveIngestPath(brain, "x.md") assert.Error(t, err, "ingest must refuse a symlinked raw/") notes, err := api.ListPending(brain) assert.Error(t, err, "pending must refuse a symlinked raw/") assert.Empty(t, notes) _, err = api.PromoteNote(brain, api.PromoteOptions{Filename: "x.md", Wing: "testing", Hall: "facts"}) assert.Error(t, err, "promote must refuse a symlinked raw/") }