once-add/internal/deploy/deploy_test.go
28allday 68c64265a6 once-add: deploy + remove Once apps at <name>.local
A small Go tool that wraps the `/etc/hosts` + `once deploy --disable-tls`
recipe needed to host a Once app on a single-label `<name>.local`. Adds
the hosts entry, runs the deploy, polls /up, and on removal cleans the
hosts line that the Once UI orphans (including the orphan-only case,
where the app is already gone).

Bubble Tea TUI for interactive use; non-interactive CLI for scripts.
Curated apps live in /etc/once-add/apps.toml; seeded with Writebook +
Campfire on first run.
2026-05-28 07:23:07 +01:00

94 lines
2.8 KiB
Go

package deploy
import (
"strings"
"testing"
)
func TestValidateName(t *testing.T) {
good := []string{"book", "chat", "a", "my-app", "app1", "1book"}
for _, n := range good {
if err := ValidateName(n); err != nil {
t.Errorf("ValidateName(%q) = %v, want nil", n, err)
}
}
bad := []string{"", "Book", "book.local", "-book", "book-", "a_b", "café", "two words"}
for _, n := range bad {
if err := ValidateName(n); err == nil {
t.Errorf("ValidateName(%q) = nil, want error", n)
}
}
long := make([]byte, 64)
for i := range long {
long[i] = 'a'
}
if err := ValidateName(string(long)); err == nil {
t.Errorf("ValidateName(64 chars) = nil, want error")
}
}
func TestParseHosts(t *testing.T) {
// Two app containers' `once` labels concatenated, as docker inspect emits.
blob := `{"image":"ghcr.io/basecamp/writebook","host":"Book.local","tls":false}
{"host":"chat.local","image":"once-campfire"}
{"host":"public.example.com"}`
got := parseHosts(blob)
if !got["book.local"] { // lowercased
t.Errorf("expected book.local in %v", got)
}
if !got["chat.local"] {
t.Errorf("expected chat.local in %v", got)
}
if got["public.example.com"] {
t.Errorf("non-.local host should be excluded: %v", got)
}
if len(got) != 2 {
t.Errorf("got %d hosts, want 2: %v", len(got), got)
}
}
func TestHost(t *testing.T) {
if Host("book") != "book.local" {
t.Errorf("Host(book) = %q", Host("book"))
}
}
func TestFilterHostLines(t *testing.T) {
in := "127.0.1.1\tdevbox\n" +
"127.0.0.1 book.local\n" +
"127.0.0.1 mybook.local\n" + // must NOT match book.local
"127.0.0.1 book.local2\n" + // must NOT match book.local
"127.0.0.1 chat.local\n"
out, removed := filterHostLines(in, "book.local")
if !removed {
t.Fatal("expected book.local to be removed")
}
if strings.Contains(out, "\n127.0.0.1 book.local\n") || strings.HasSuffix(out, "127.0.0.1 book.local") {
t.Errorf("book.local line still present:\n%s", out)
}
for _, keep := range []string{"devbox", "mybook.local", "book.local2", "chat.local"} {
if !strings.Contains(out, keep) {
t.Errorf("filter wrongly dropped %q:\n%s", keep, out)
}
}
if _, removed := filterHostLines(in, "absent.local"); removed {
t.Error("absent.local should report nothing removed")
}
}
func TestRemoveDoneMsg(t *testing.T) {
cases := []struct {
appRemoved, hostsRemoved bool
want string
}{
{true, true, "Removed book.local"},
{true, false, "Removed book.local (no /etc/hosts entry to clean)"},
{false, true, "Cleaned orphan /etc/hosts entry for book.local"},
{false, false, "Nothing to remove for book.local"},
}
for _, c := range cases {
if got := removeDoneMsg("book.local", c.appRemoved, c.hostsRemoved); got != c.want {
t.Errorf("removeDoneMsg(_, %v, %v) = %q, want %q", c.appRemoved, c.hostsRemoved, got, c.want)
}
}
}