mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
* test(sites): reproduce stale failed domain claims Signed-off-by: RissRIce <jsdavid278@gmail.com> * fix(sites): roll back failed domain claims Signed-off-by: RissRIce <jsdavid278@gmail.com> --------- Signed-off-by: RissRIce <jsdavid278@gmail.com>
240 lines
6.3 KiB
Go
240 lines
6.3 KiB
Go
package sites
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/profullstack/agentbbs/internal/store"
|
|
)
|
|
|
|
func TestNormalizeAndValid(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
norm string
|
|
valid bool
|
|
}{
|
|
{"Chovy.com", "chovy.com", true},
|
|
{" https://Example.COM/ ", "example.com", true},
|
|
{"sub.example.co.uk.", "sub.example.co.uk", true},
|
|
{"localhost", "localhost", false}, // no TLD
|
|
{"bad_domain.com", "bad_domain.com", false}, // underscore
|
|
{"../etc/passwd", "../etc/passwd", false},
|
|
{"", "", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := Normalize(c.in); got != c.norm {
|
|
t.Errorf("Normalize(%q) = %q, want %q", c.in, got, c.norm)
|
|
}
|
|
if got := Valid(Normalize(c.in)); got != c.valid {
|
|
t.Errorf("Valid(Normalize(%q)) = %v, want %v", c.in, got, c.valid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManagerAddRemoveSyncAsk(t *testing.T) {
|
|
dir := t.TempDir()
|
|
st, err := store.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
|
|
m, err := NewManager(st, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add creates the DB row and a symlink to the user's public_html.
|
|
if _, err := m.Add("Chovy.com", "chovy"); err != nil {
|
|
t.Fatalf("Add: %v", err)
|
|
}
|
|
link := filepath.Join(dir, "domains", "chovy.com")
|
|
target, err := os.Readlink(link)
|
|
if err != nil {
|
|
t.Fatalf("expected symlink at %s: %v", link, err)
|
|
}
|
|
if want := filepath.Join(dir, "users", "chovy", "public_html"); target != want {
|
|
t.Errorf("symlink target = %q, want %q", target, want)
|
|
}
|
|
|
|
// A different user cannot steal a mapped domain.
|
|
if _, err := m.Add("chovy.com", "someoneelse"); err != store.ErrDomainTaken {
|
|
t.Errorf("expected ErrDomainTaken, got %v", err)
|
|
}
|
|
|
|
// Invalid domains are rejected.
|
|
if _, err := m.Add("not a domain", "chovy"); err != ErrInvalidDomain {
|
|
t.Errorf("expected ErrInvalidDomain, got %v", err)
|
|
}
|
|
|
|
// Ask endpoint: 200 for mapped, 404 for unmapped, 400 for junk.
|
|
h := m.AskHandler()
|
|
for _, c := range []struct {
|
|
domain string
|
|
code int
|
|
}{
|
|
{"chovy.com", http.StatusOK},
|
|
{"unmapped.com", http.StatusNotFound},
|
|
{"localhost", http.StatusBadRequest},
|
|
} {
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/check?domain="+c.domain, nil)
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != c.code {
|
|
t.Errorf("ask %q = %d, want %d", c.domain, rec.Code, c.code)
|
|
}
|
|
}
|
|
|
|
// Sync rebuilds the farm from the DB after the link is removed out-of-band.
|
|
if err := os.Remove(link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.Sync(); err != nil {
|
|
t.Fatalf("Sync: %v", err)
|
|
}
|
|
if _, err := os.Readlink(link); err != nil {
|
|
t.Errorf("Sync did not restore symlink: %v", err)
|
|
}
|
|
|
|
// Remove drops both the row and the link.
|
|
if _, err := m.Remove("chovy.com", "chovy"); err != nil {
|
|
t.Fatalf("Remove: %v", err)
|
|
}
|
|
if _, err := os.Lstat(link); !os.IsNotExist(err) {
|
|
t.Errorf("expected symlink gone, got err=%v", err)
|
|
}
|
|
if _, ok, _ := st.DomainUser("chovy.com"); ok {
|
|
t.Error("expected domain unmapped in store")
|
|
}
|
|
}
|
|
|
|
func TestManagerAddRollsBackNewMappingWhenLinkCreationFails(t *testing.T) {
|
|
dir := t.TempDir()
|
|
st, err := store.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
|
|
m, err := NewManager(st, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
domain := "blocked.example.com"
|
|
blocked := filepath.Join(dir, "domains", domain)
|
|
if err := os.MkdirAll(blocked, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(blocked, "keep"), []byte("occupied"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := m.Add(domain, "alice"); err == nil {
|
|
t.Fatal("Add succeeded despite an occupied domain link path")
|
|
}
|
|
if owner, ok, err := st.DomainUser(domain); err != nil {
|
|
t.Fatal(err)
|
|
} else if ok {
|
|
t.Fatalf("failed Add left domain mapped to %q", owner)
|
|
}
|
|
}
|
|
|
|
func TestManagerAddPreservesExistingMappingWhenLinkRepairFails(t *testing.T) {
|
|
dir := t.TempDir()
|
|
st, err := store.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
|
|
m, err := NewManager(st, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
domain := "blocked.example.com"
|
|
if created, err := st.MapDomain(domain, "alice"); err != nil {
|
|
t.Fatal(err)
|
|
} else if !created {
|
|
t.Fatal("expected a new domain mapping")
|
|
}
|
|
blocked := filepath.Join(dir, "domains", domain)
|
|
if err := os.MkdirAll(blocked, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(blocked, "keep"), []byte("occupied"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := m.Add(domain, "alice"); err == nil {
|
|
t.Fatal("Add succeeded despite an occupied domain link path")
|
|
}
|
|
if owner, ok, err := st.DomainUser(domain); err != nil {
|
|
t.Fatal(err)
|
|
} else if !ok || owner != "alice" {
|
|
t.Fatalf("existing mapping changed after failed repair: owner=%q ok=%v", owner, ok)
|
|
}
|
|
}
|
|
|
|
func TestSyncRemovesStaleDomainEntries(t *testing.T) {
|
|
dir := t.TempDir()
|
|
st, err := store.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
|
|
m, err := NewManager(st, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stale := filepath.Join(dir, "domains", "stale.example.com")
|
|
if err := os.WriteFile(stale, []byte("stale"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := m.Sync(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Lstat(stale); !os.IsNotExist(err) {
|
|
t.Errorf("expected stale domain entry removed, got err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestAskUserSubdomain(t *testing.T) {
|
|
t.Setenv("AGENTBBS_HOST", "bbs.profullstack.com")
|
|
dir := t.TempDir()
|
|
st, err := store.Open(filepath.Join(dir, "test.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer st.Close()
|
|
if _, err := st.EnsureUser("alice", "member", "SHA256:aaa"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
m, err := NewManager(st, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := m.AskHandler()
|
|
|
|
for _, c := range []struct {
|
|
domain string
|
|
code int
|
|
}{
|
|
{"alice.bbs.profullstack.com", http.StatusOK}, // registered member → cert allowed
|
|
{"nobody.bbs.profullstack.com", http.StatusNotFound}, // no such user
|
|
{"a.b.bbs.profullstack.com", http.StatusNotFound}, // multi-label, not a user subdomain
|
|
{"bbs.profullstack.com", http.StatusNotFound}, // apex is not a user subdomain
|
|
} {
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/check?domain="+c.domain, nil)
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != c.code {
|
|
t.Errorf("ask %q = %d, want %d", c.domain, rec.Code, c.code)
|
|
}
|
|
}
|
|
}
|