package foreign
import (
"strings"
"testing"
)
func TestForeign_BasicWrap(t *testing.T) {
got := Foreign("hello")
want := "\n\n\nhello\n\n\n"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestForeign_LeadingTrailingBlankLines(t *testing.T) {
// Parser requires a blank line before the opener (CM §4.6). Verify
// the helper emits exactly that.
got := Foreign("body")
if !strings.HasPrefix(got, "\n\n\n") {
t.Errorf("missing blank line before opener: %q", got)
}
if !strings.HasSuffix(got, "\n\n\n") {
t.Errorf("missing blank line after closer: %q", got)
}
}
func TestForeign_NormalizesCRLF(t *testing.T) {
got := Foreign("line1\r\nline2\r\nline3")
if strings.Contains(got, "\r") {
t.Errorf("CRLF not normalized: %q", got)
}
if !strings.Contains(got, "line1\nline2\nline3") {
t.Errorf("body content lost: %q", got)
}
}
func TestForeign_NormalizesBareCR(t *testing.T) {
got := Foreign("line1\rline2")
if strings.Contains(got, "\r") {
t.Errorf("bare CR not normalized: %q", got)
}
if !strings.Contains(got, "line1\nline2") {
t.Errorf("body content lost: %q", got)
}
}
func TestForeign_EscapesSentinelClose(t *testing.T) {
// Unbalanced in body would terminate the outer
// block early. Verify it's escaped to literal text — only the
// leading "<" is replaced with "<"; the trailing ">" stays
// literal (rendering as visible ">" in HTML).
got := Foreign("foo\n\nbar")
if strings.Contains(got, "\n\nbar") {
t.Errorf("unbalanced close not escaped: %q", got)
}
if !strings.Contains(got, "</gno-foreign>") {
t.Errorf("expected escaped close: %q", got)
}
}
func TestForeign_EscapesSentinelOpen(t *testing.T) {
// A bare opener inside body (without matching close in body)
// would be picked up as a nested inner foreign. Escape it.
got := Foreign("foo\n\nbar")
if !strings.Contains(got, "\n<gno-foreign>\n") {
t.Errorf("expected escaped inner opener: %q", got)
}
}
func TestForeign_EscapesIndentedSentinel(t *testing.T) {
// Parser strips 0-3 leading spaces before matching; helper
// must mirror that to catch indented sentinels.
got := Foreign("foo\n \nbar")
if !strings.Contains(got, "</gno-foreign>") {
t.Errorf("indented sentinel not escaped: %q", got)
}
}
func TestForeign_EscapesAttributeBearingOpener(t *testing.T) {
// `` IS recognized by the parser as a
// nested opener. If left unmangled, the parser bumps framingDepth
// when it sees this line in body bytes, and then consumes the
// helper's own outer-close as a fake "inner close" — capturing
// realm content that follows the helper's output INTO the
// sandbox. Verify the helper escapes the leading "<".
got := Foreign(``)
if strings.Contains(got, ``) {
t.Errorf("attribute-bearing opener was NOT mangled — escape vector:\n%q", got)
}
if !strings.Contains(got, `<gno-foreign label="x">`) {
t.Errorf("expected leading '<' escaped: %q", got)
}
}
func TestForeign_EscapesAttributeBearingCloser(t *testing.T) {
// golang.org/x/net/html zeroes the Attr slice on end tags, so the
// parser recognizes ANY `` form as a sentinel
// close. Leaving such a line unmangled in body bytes would close
// the outer block early (framingDepth=0 case in the parser's
// Continue), letting body content that follows render OUTSIDE
// the sandbox at top level. Verify the helper escapes it.
got := Foreign(``)
if strings.Contains(got, `> `) {
t.Errorf("attribute-bearing closer was NOT mangled — escape vector:\n%q", got)
}
if !strings.Contains(got, `</gno-foreign label="x">`) {
t.Errorf("expected leading '<' escaped: %q", got)
}
}
func TestForeign_EscapesGnoForeignWithExtraContent(t *testing.T) {
// ` extra` would not be recognized as a sentinel by
// the parser (tokenizer yields 2 tokens). But the helper is
// over-inclusive on purpose: anything that looks like a
// gno-foreign tag opener or closer gets mangled. Predictable
// behavior beats narrow matching that diverges from the parser
// on attribute-bearing forms.
got := Foreign(" extra text")
if !strings.Contains(got, "<gno-foreign> extra text") {
t.Errorf("expected over-inclusive mangle of ` extra`: %q", got)
}
}
func TestForeign_EscapesCaseVariantClose(t *testing.T) {
// The renderer-side parser recognizes tags case-insensitively
// (goldmark's tokenizer lowercases tag names). A case-variant close
// left unescaped would terminate the outer block early and let
// trailing body render OUTSIDE the sandbox. The helper must escape
// `` just like ``.
got := Foreign("foo\n\nbar")
if strings.Contains(got, "\n\nbar") {
t.Errorf("case-variant close not escaped — sandbox-escape vector: %q", got)
}
if !strings.Contains(got, "</GNO-FOREIGN>") {
t.Errorf("expected escaped case-variant close: %q", got)
}
}
func TestForeign_EscapesCaseVariantOpen(t *testing.T) {
// A case-variant opener left unescaped would be picked up by the
// parser as a nested inner foreign (framingDepth++), causing the
// helper's own outer-close to be consumed as a fake inner close and
// capturing trailing realm content into the sandbox.
got := Foreign("foo\n\nbar")
if !strings.Contains(got, "\n<GNO-FOREIGN>\n") {
t.Errorf("case-variant opener not escaped — capture vector: %q", got)
}
}
func TestForeign_EscapesMixedCaseSentinel(t *testing.T) {
// Mixed-case forms (the most likely hand-crafted evasion) must also
// be caught, including the attribute-bearing mixed-case opener.
for _, in := range []string{
"",
"",
``,
"",
} {
got := Foreign(in)
if !strings.Contains(got, "<") {
t.Errorf("mixed-case sentinel %q not escaped: %q", in, got)
}
}
}
func TestForeignWithLabel_StripsBidiAndZeroWidth(t *testing.T) {
// A bidi-override (U+202E) or zero-width (U+200C) run in the label
// is invisible-spoof payload; it must be stripped before the label
// is spliced into the opener attribute.
got := ForeignWithLabel("a\u202eb\u200cc", "body")
if !strings.Contains(got, `label="abc"`) {
t.Errorf("expected bidi/zero-width-stripped label: %q", got)
}
}
func TestForeignWithLabel_UnicodeSeparatorsBecomeSpaces(t *testing.T) {
// U+2028 / U+2029 / U+0085 must not survive in the single-line
// opener attribute; they fold to spaces like ASCII line breaks.
got := ForeignWithLabel("a\u2028b\u2029c\u0085d", "body")
if !strings.Contains(got, `label="a b c d"`) {
t.Errorf("expected space-folded label: %q", got)
}
}
func TestForeign_EscapesUnrelatedForeignPrefix(t *testing.T) {
// `` is a different tag the parser would NOT treat as
// a sentinel, but the escaper is deliberately over-inclusive (it
// matches the `")
if !strings.Contains(got, "<gno-foreignx>") {
t.Errorf("expected over-inclusive escape of : %q", got)
}
}
func TestForeign_EscapesSlashAndFormFeedClosers(t *testing.T) {
// REGRESSION: goldmark's tokenizer ends a tag name at `/` and at
// form-feed (both HTML tag-name terminators), so ``
// and `` are recognized as CLOSERS by the parser. A
// body line carrying one of these, if it reached the parser
// unescaped, would close the outer block early — a sandbox escape.
// The over-inclusive escaper must neutralize every such variant.
for _, in := range []string{
"foo\n\nbar",
"foo\n\nbar",
"foo\n\nbar",
"foo\n\nbar",
} {
got := Foreign(in)
if !strings.Contains(got, "<") {
t.Errorf("variant not escaped (sandbox-escape vector): %q -> %q", in, got)
}
}
}
func TestForeign_EmptyBody(t *testing.T) {
got := Foreign("")
want := "\n\n\n\n\n\n"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestForeign_PreservesInternalBlankLines(t *testing.T) {
got := Foreign("para1\n\npara2")
if !strings.Contains(got, "para1\n\npara2") {
t.Errorf("internal blank line lost: %q", got)
}
}
func TestForeignWithLabel_BasicWrap(t *testing.T) {
got := ForeignWithLabel("My Label", "body")
want := "\n\n\nbody\n\n\n"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestForeignWithLabel_EmptyLabelFallsBack(t *testing.T) {
// An empty label (or one that becomes empty after sanitization)
// should be identical to Foreign — no attribute on the opener.
got := ForeignWithLabel("", "body")
if strings.Contains(got, "label=") {
t.Errorf("empty label should not emit attribute: %q", got)
}
if got != Foreign("body") {
t.Errorf("empty label should match Foreign: got %q vs %q", got, Foreign("body"))
}
}
func TestForeignWithLabel_WhitespaceOnlyLabelFallsBack(t *testing.T) {
got := ForeignWithLabel(" ", "body")
if strings.Contains(got, "label=") {
t.Errorf("whitespace label should not emit attribute: %q", got)
}
}
func TestForeignWithLabel_EscapesAmpAndQuote(t *testing.T) {
got := ForeignWithLabel(`Tom & "Jerry"`, "body")
if !strings.Contains(got, `label="Tom & "Jerry""`) {
t.Errorf("expected & and \" escaped: %q", got)
}
}
func TestForeignWithLabel_EscapesAngleBrackets(t *testing.T) {
// An unescaped `>` would close the opener tag early. Must be
// escaped.
got := ForeignWithLabel(`ac`, "body")
if !strings.Contains(got, `label="a<b>c"`) {
t.Errorf("expected < and > escaped: %q", got)
}
}
func TestForeignWithLabel_StripsNUL(t *testing.T) {
got := ForeignWithLabel("a\x00b\x00c", "body")
if strings.Contains(got, "\x00") {
t.Errorf("NUL not stripped: %q", got)
}
if !strings.Contains(got, `label="abc"`) {
t.Errorf("expected NUL-stripped label: %q", got)
}
}
func TestForeignWithLabel_ControlCharsBecomeSpaces(t *testing.T) {
// Newlines and tabs would break the single-line opener required
// by the parser. They must become spaces.
got := ForeignWithLabel("line1\nline2\ttab", "body")
if strings.Contains(got, "\nline2") || strings.Contains(got, "\t") {
t.Errorf("control chars not converted: %q", got)
}
if !strings.Contains(got, "line1 line2 tab") {
t.Errorf("expected space-separated label: %q", got)
}
}
func TestForeignWithLabel_TrimsLeadingTrailingSpace(t *testing.T) {
got := ForeignWithLabel(" hello ", "body")
if !strings.Contains(got, `label="hello"`) {
t.Errorf("expected trimmed label: %q", got)
}
}
func TestForeignWithLabel_PreservesBodyNormalization(t *testing.T) {
// Body normalization (CRLF, sentinel escaping) must still apply
// in the labeled variant.
got := ForeignWithLabel("L", "a\r\n\r\nb")
if strings.Contains(got, "\r") {
t.Errorf("CRLF not normalized in labeled variant: %q", got)
}
if !strings.Contains(got, "</gno-foreign>") {
t.Errorf("sentinel not escaped in labeled variant: %q", got)
}
}
func TestMaxBlocksPerRender(t *testing.T) {
// Surfaces gnoweb's per-render foreign-block cap to realms via the
// chain/markdown native. Pin the contract value; bump here if the
// renderer cap changes.
if got := MaxBlocksPerRender(); got != 256 {
t.Errorf("MaxBlocksPerRender() = %d, want 256", got)
}
}