package core
import (
"errors"
"strings"
"testing"
)
func TestParseDocID(t *testing.T) {
tests := []struct {
name string
in string
wantPrefix string
wantSeq string
ok bool
}{
{"spec", "SPEC-0007", "SPEC", "0007", true},
{"rfc", "RFC-1", "RFC", "1", true},
{"single letter prefix", "X-42", "X", "42", true},
{"digits in prefix", "C4-0001", "C4", "0001", true},
{"all zeros", "SPEC-0000", "SPEC", "0000", true},
{"max prefix", strings.Repeat("A", MaxDocIDPrefixLen) + "-1", strings.Repeat("A", MaxDocIDPrefixLen), "1", true},
{"max seq", "SPEC-" + strings.Repeat("9", MaxDocIDSeqLen), "SPEC", strings.Repeat("9", MaxDocIDSeqLen), true},
{"empty", "", "", "", false},
{"no separator", "SPEC0007", "", "", false},
{"two separators", "HOME-OPS-0001", "", "", false},
{"double dash", "SPEC--1", "", "", false},
{"empty prefix", "-0007", "", "", false},
{"empty seq", "SPEC-", "", "", false},
{"lowercase prefix", "spec-0007", "", "", false},
{"mixed case prefix", "Spec-0007", "", "", false},
{"digit-leading prefix", "0SPEC-1", "", "", false},
{"underscore in prefix", "SPEC_A-1", "", "", false},
{"dot in prefix", "SPEC.A-1", "", "", false},
{"non-digit seq", "SPEC-007a", "", "", false},
{"signed seq", "SPEC-+1", "", "", false},
{"hex seq", "SPEC-0x7", "", "", false},
{"prefix too long", strings.Repeat("A", MaxDocIDPrefixLen+1) + "-1", "", "", false},
{"seq too long", "SPEC-" + strings.Repeat("9", MaxDocIDSeqLen+1), "", "", false},
{"leading space", " SPEC-0007", "", "", false},
{"trailing space", "SPEC-0007 ", "", "", false},
{"inner space", "SPEC - 0007", "", "", false},
{"newline", "SPEC-0007\n", "", "", false},
// Homograph: Cyrillic С/Р/Е/С look identical to Latin in most fonts.
// Accepting them would let two visually identical IDs both register.
{"cyrillic homograph", "СПЕС-0007", "", "", false},
{"fullwidth digits", "SPEC-0007", "", "", false},
{"arabic-indic digits", "SPEC-٠٠٠٧", "", "", false},
{"path injection", "SPEC-0007/../x", "", "", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
id, err := ParseDocID(tc.in)
if (err == nil) != tc.ok {
t.Fatalf("ParseDocID(%q) = %+v, %v, want ok=%v", tc.in, id, err, tc.ok)
}
if !tc.ok {
if !errors.Is(err, ErrInvalidDocID) {
t.Fatalf("error %v is not ErrInvalidDocID", err)
}
return
}
if id.Prefix != tc.wantPrefix || id.Seq != tc.wantSeq {
t.Fatalf("ParseDocID(%q) = %+v, want {%q %q}", tc.in, id, tc.wantPrefix, tc.wantSeq)
}
if got := id.String(); got != tc.in {
t.Fatalf("String() = %q, want round-trip %q", got, tc.in)
}
})
}
}
// Leading zeros are significant: the registry is global and "SPEC-7" is a
// different key from "SPEC-0007". Normalizing them together would silently
// merge two documents.
func TestDocIDLeadingZerosAreSignificant(t *testing.T) {
a, err := ParseDocID("SPEC-7")
if err != nil {
t.Fatal(err)
}
b, err := ParseDocID("SPEC-0007")
if err != nil {
t.Fatal(err)
}
if a == b {
t.Fatalf("SPEC-7 and SPEC-0007 must not compare equal, got %+v", a)
}
if a.String() == b.String() {
t.Fatal("SPEC-7 and SPEC-0007 must not render identically")
}
}
func TestValidateDocID(t *testing.T) {
if err := ValidateDocID("SPEC-0007"); err != nil {
t.Fatalf("ValidateDocID(SPEC-0007) = %v, want nil", err)
}
if err := ValidateDocID("spec-0007"); !errors.Is(err, ErrInvalidDocID) {
t.Fatalf("ValidateDocID(spec-0007) = %v, want ErrInvalidDocID", err)
}
}