mdedit/ is gone. Its functionality moved into browse's preview plugin
(browse/js/preview-markdown.js) — YAML front matter editing, outline,
and on-demand DOCX/HTML/PDF download all happen there. Browse is the
default_tool for working/ + reviewing/ as of the previous commit, so
existing URLs of the form /<project>/working land on browse without
operator action.
Removed:
• mdedit/ source tree (Toast UI app, CSS, JS, template, build.sh)
• zddc/internal/apps/embedded/mdedit.html (//go:embed blob)
• tests/mdedit.spec.js + the "mdedit" project in playwright.config.js
• mdedit entries in zddc/internal/apps/embed.go (//go:embed, var,
switch case in EmbeddedBytes)
• "mdedit" in zddc/internal/zddc/validate.go AppNames + the matching
error-message app list
• "mdedit.html" branch in zddc/internal/apps/handler.go MatchAppHTML
• mdedit case in tests (handler_test.go, validate_test.go,
zddchandler_test.go) — test fixtures now use browse/classifier
• mdedit from build (per-tool build.sh loop, tool-list literals,
composer cards) and shared/build-lib.sh ZDDC_RELEASE_TOOLS
• mdedit from freshen-channel's tool list and usage banner
• mdedit-specific paragraphs in AGENTS.md and ARCHITECTURE.md;
Markdown Editor section in ARCHITECTURE.md rewritten to point at
browse/js/preview-markdown.js
• mdedit from CLAUDE.md, README.md, zddc/README.md tool lists
Historical mdedit_v*.html / mdedit_v*.html.sig files in
/srv/zddc/releases/ on the deploy host are immutable history — they
stay where they are. The next ./build release cut will simply not
produce new mdedit_v* artifacts.
236 lines
6 KiB
Go
236 lines
6 KiB
Go
package zddc
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestValidatePattern(t *testing.T) {
|
|
cases := []struct {
|
|
pattern string
|
|
ok bool
|
|
}{
|
|
{"alice@example.com", true},
|
|
{"*@example.com", true},
|
|
{"alice@*", true},
|
|
{"*", true},
|
|
{"", false},
|
|
{" alice@example.com", false},
|
|
{"alice@example.com ", false},
|
|
{"alice @example.com", false},
|
|
{"alice@ex ample.com", false},
|
|
{"alice@@example.com", false},
|
|
{"@example.com", false},
|
|
{"alice@", false},
|
|
{"@", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.pattern, func(t *testing.T) {
|
|
err := ValidatePattern(tc.pattern)
|
|
if tc.ok && err != nil {
|
|
t.Errorf("ValidatePattern(%q) = %v, want nil", tc.pattern, err)
|
|
}
|
|
if !tc.ok && err == nil {
|
|
t.Errorf("ValidatePattern(%q) = nil, want error", tc.pattern)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateFile(t *testing.T) {
|
|
zf := ZddcFile{
|
|
Title: "ok",
|
|
ACL: ACLRules{Allow: []string{"good@example.com", "@bad"}, Deny: []string{"two@@ats"}},
|
|
Admins: []string{"@nobody"},
|
|
}
|
|
errs := ValidateFile(zf)
|
|
// expect 3 errors
|
|
if len(errs) != 3 {
|
|
t.Fatalf("got %d errors, want 3: %+v", len(errs), errs)
|
|
}
|
|
wantFields := map[string]bool{
|
|
"acl.allow[1]": false,
|
|
"acl.deny[0]": false,
|
|
"admins[0]": false,
|
|
}
|
|
for _, e := range errs {
|
|
if _, ok := wantFields[e.Field]; !ok {
|
|
t.Errorf("unexpected error field: %q", e.Field)
|
|
continue
|
|
}
|
|
wantFields[e.Field] = true
|
|
}
|
|
for f, seen := range wantFields {
|
|
if !seen {
|
|
t.Errorf("missing error for field %q", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateAppSourceSpec(t *testing.T) {
|
|
cases := []struct {
|
|
spec string
|
|
ok bool
|
|
}{
|
|
// Channel shorthand (with and without leading colon)
|
|
{"stable", true},
|
|
{"beta", true},
|
|
{"alpha", true},
|
|
{":stable", true},
|
|
{":beta", true},
|
|
{":alpha", true},
|
|
// Version pin shorthand (full, partial, with/without leading 'v')
|
|
{"v0.0.4", true},
|
|
{"0.0.4", true},
|
|
{"v0.0", true},
|
|
{"0.0", true},
|
|
{"v0", true},
|
|
{"0", true},
|
|
{"v1.2.3", true},
|
|
{":v0.0.4", true},
|
|
{":0.0.4", true},
|
|
// URLs
|
|
{"https://zddc.varasys.io/releases/archive_stable.html", true},
|
|
{"http://my-fork.example.com/archive.html", true},
|
|
{"https://my-mirror.example/releases", true}, // URL-prefix only
|
|
{"https://my-mirror.example/releases:stable", true}, // URL-prefix + channel
|
|
{"https://my-mirror.example/releases:v0.0.4", true}, // URL-prefix + version
|
|
{"https://my-mirror.example:8080/releases", true}, // URL with port
|
|
{"https://my-mirror.example:8080/releases:stable", true}, // URL with port + channel
|
|
// Paths
|
|
{"/abs/path.html", true},
|
|
{"./local.html", true},
|
|
{"../sibling.html", true},
|
|
// Errors
|
|
{"", false},
|
|
{" stable", false},
|
|
{"stable ", false},
|
|
{"with space", false},
|
|
{"https://", false},
|
|
{"https://host/path/file.html:stable", false}, // .html URL with suffix
|
|
{"random-thing", false},
|
|
{":", false},
|
|
{":random", false},
|
|
{"v", false},
|
|
{"v0.", false},
|
|
{".0.0", false},
|
|
{"v0.0.0.0", false},
|
|
{"v0.a.0", false},
|
|
{"https://my-mirror.example/releases:bogus", false}, // bad channel suffix
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.spec, func(t *testing.T) {
|
|
err := ValidateAppSourceSpec(tc.spec)
|
|
if tc.ok && err != nil {
|
|
t.Errorf("ValidateAppSourceSpec(%q) = %v, want nil", tc.spec, err)
|
|
}
|
|
if !tc.ok && err == nil {
|
|
t.Errorf("ValidateAppSourceSpec(%q) = nil, want error", tc.spec)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidAppsKey(t *testing.T) {
|
|
cases := []struct {
|
|
key string
|
|
ok bool
|
|
}{
|
|
{"default", true},
|
|
{"archive", true},
|
|
{"transmittal", true},
|
|
{"classifier", true},
|
|
{"browse", true},
|
|
{"landing", true},
|
|
{"unknown", false},
|
|
{"", false},
|
|
{"DEFAULT", false}, // case-sensitive
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.key, func(t *testing.T) {
|
|
if got := IsValidAppsKey(tc.key); got != tc.ok {
|
|
t.Errorf("IsValidAppsKey(%q) = %v, want %v", tc.key, got, tc.ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateFile_Apps(t *testing.T) {
|
|
zf := ZddcFile{
|
|
Apps: map[string]string{
|
|
"archive": "stable", // ok
|
|
"classifier": "v0.0.4", // ok
|
|
"default": "https://zddc.varasys.io/releases:stable", // ok (default key + URL+channel)
|
|
"transmittal": ":beta", // ok (channel-only)
|
|
"browse": "https://my-mirror.example/releases", // ok (URL-prefix only)
|
|
"unknown": "stable", // unknown app
|
|
"landing": "what is this", // bad spec
|
|
},
|
|
}
|
|
errs := ValidateFile(zf)
|
|
want := map[string]bool{
|
|
"apps.unknown": false,
|
|
"apps.landing": false,
|
|
}
|
|
for _, e := range errs {
|
|
if _, ok := want[e.Field]; ok {
|
|
want[e.Field] = true
|
|
} else {
|
|
t.Errorf("unexpected error field: %q (%s)", e.Field, e.Message)
|
|
}
|
|
}
|
|
for f, seen := range want {
|
|
if !seen {
|
|
t.Errorf("missing error for field %q (got: %+v)", f, errs)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectName(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
ok bool
|
|
}{
|
|
{"alpha", true},
|
|
{"Alpha", true},
|
|
{"a", true},
|
|
{"a1", true},
|
|
{"a-1", true},
|
|
{"a_b", true},
|
|
{"123-project", true},
|
|
{"Site-3", true},
|
|
{"", false},
|
|
{".hidden", false},
|
|
{"_template", false},
|
|
{"-leading-dash", false},
|
|
{"foo bar", false},
|
|
{"foo/bar", false},
|
|
{"foo\\bar", false},
|
|
{"foo.bar", false},
|
|
{"..", false},
|
|
{".", false},
|
|
{string(make([]byte, 65)), false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := ValidateProjectName(tc.name)
|
|
if tc.ok && err != nil {
|
|
t.Errorf("ValidateProjectName(%q) = %v, want nil", tc.name, err)
|
|
}
|
|
if !tc.ok && err == nil {
|
|
t.Errorf("ValidateProjectName(%q) = nil, want error", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateFileTitleLength(t *testing.T) {
|
|
long := make([]byte, 201)
|
|
for i := range long {
|
|
long[i] = 'a'
|
|
}
|
|
zf := ZddcFile{Title: string(long)}
|
|
errs := ValidateFile(zf)
|
|
if len(errs) != 1 || errs[0].Field != "title" {
|
|
t.Fatalf("expected one title-length error, got %+v", errs)
|
|
}
|
|
}
|