81 lines
3.3 KiB
Go
81 lines
3.3 KiB
Go
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 TestResolvePendingPromoteRejectAndReadIngestRefuseASymlinkedRawRoot(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/")
|
|
_, err = api.RejectNote(brain, api.RejectOptions{Filename: "x.md"})
|
|
assert.Error(t, err, "reject must refuse a symlinked raw/")
|
|
text, err := api.ReadIngestFile(brain, filepath.Join(elsewhere, "x.md"))
|
|
assert.Error(t, err, "ReadIngestFile must refuse a symlinked raw/")
|
|
assert.NotContains(t, text, sentinel153)
|
|
// The watcher's refusal is tested in internal/watcher.
|
|
}
|