1package domains23import (4 "context"5 "encoding/json"6 "errors"7 "net"8 "net/http"9 "net/http/httptest"10 "strings"11 "testing"1213 "github.com/FelineStateMachine/tinyrelay/internal/catalog"14)1516const domainOwner = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"1718func testService(t *testing.T) (*Service, *catalog.Catalog, string) {19 t.Helper()20 cat, err := catalog.Open(context.Background(), t.TempDir())21 if err != nil {22 t.Fatal(err)23 }24 t.Cleanup(func() { _ = cat.Close() })25 tenant, err := cat.Create(context.Background(), catalog.CreateOptions{Name: "relay", Owner: domainOwner})26 if err != nil {27 t.Fatal(err)28 }29 returnMust := func() *Service {30 s, newErr := New(Config{Catalog: cat, TenantID: tenant.ID, BaseHost: "tiny.example", RelayURL: "https://tiny.example/relay", Owner: func(actor string) bool { return actor == domainOwner }, ResolveDNS: func(context.Context, string) ([]net.IP, error) { return []net.IP{net.ParseIP("203.0.113.7")}, nil }})31 if newErr != nil {32 t.Fatal(newErr)33 }34 return s35 }36 return returnMust(), cat, tenant.ID37}3839func TestDomainManagementUsesExplicitLocalMapping(t *testing.T) {40 s, cat, tenantID := testService(t)41 ctx := context.Background()42 view, err := s.Execute(ctx, domainOwner, "adddomain", raws(`"relay.alice.example"`, `null`))43 if err != nil {44 t.Fatal(err)45 }46 if view.(View).Status != "configured" || !view.(View).Ready {47 t.Fatalf("add view = %+v", view)48 }49 resolved, err := cat.ResolveHost(ctx, "relay.alice.example")50 if err != nil || resolved.ID != tenantID {51 t.Fatalf("resolved = %+v, err=%v", resolved, err)52 }53 checked, err := s.Execute(ctx, domainOwner, "checkdomain", raws(`"relay.alice.example"`))54 if err != nil || len(checked.(View).Addresses) != 1 {55 t.Fatalf("check = %+v, err=%v", checked, err)56 }57 if _, err := s.Execute(ctx, "not-owner", "adddomain", raws(`"other.example"`)); err == nil {58 t.Fatal("non-owner domain mutation succeeded")59 }60 if _, err := s.Execute(ctx, domainOwner, "adddomain", raws(`"*.example"`)); err == nil {61 t.Fatal("wildcard accepted")62 }63 if _, err := s.Execute(ctx, domainOwner, "adddomain", raws(`"relay.tiny.example"`)); err == nil {64 t.Fatal("base-host child accepted")65 }66 if err := s.Remove(ctx, "relay.alice.example"); err != nil {67 t.Fatal(err)68 }69 if _, err := cat.ResolveHostMapping(ctx, "relay.alice.example"); !errors.Is(err, catalog.ErrHostMappingNotFound) {70 t.Fatalf("removed mapping err=%v", err)71 }72}7374func TestWebAddressDiscoveryValidationAndResponse(t *testing.T) {75 h := WebAddressHandler(AddressConfig{RelayURL: "wss://relay.example", Authorized: func(*http.Request) bool { return true }, Lookup: func(context.Context, string) (map[string]any, bool, error) {76 return map[string]any{"ids": []string{"abc"}}, true, nil77 }})78 req := httptest.NewRequest(http.MethodGet, "https://relay.example/.well-known/nostr.json?path=%2Fe%2Fabc", nil)79 resp := httptest.NewRecorder()80 h.ServeHTTP(resp, req)81 if resp.Code != http.StatusOK || !strings.Contains(resp.Body.String(), `"/e/abc"`) || !strings.Contains(resp.Body.String(), `wss://relay.example`) {82 t.Fatalf("response %d %s", resp.Code, resp.Body.String())83 }84 bad := httptest.NewRecorder()85 h.ServeHTTP(bad, httptest.NewRequest(http.MethodGet, "https://relay.example/.well-known/nostr.json?path=%2F..%2Fx", nil))86 if bad.Code != http.StatusBadRequest {87 t.Fatalf("bad path status = %d", bad.Code)88 }89 unauth := WebAddressHandler(AddressConfig{Authorized: func(*http.Request) bool { return false }})90 got := httptest.NewRecorder()91 unauth.ServeHTTP(got, httptest.NewRequest(http.MethodGet, "https://relay.example/.well-known/nostr.json?path=%2F", nil))92 if got.Code != http.StatusUnauthorized {93 t.Fatalf("unauthorized status = %d", got.Code)94 }95}9697func raws(values ...string) []json.RawMessage {98 out := make([]json.RawMessage, len(values))99 for i, value := range values {100 out[i] = json.RawMessage(value)101 }102 return out103}