1package tinyclient23import (4 "net/url"5 "strings"6 "testing"7)89func TestMobileHeaderGeneralRoutes(t *testing.T) {10 tests := []struct {11 path, title, wantTitle, wantSubtitle, wantBack string12 }{13 {"/", "tiny | relay", "Home", "tiny", ""},14 {"/social", "social | tiny", "Social", "tiny", "/"},15 {"/files", "Files | tiny", "Files", "tiny", "/"},16 {"/wiki", "Wiki | tiny", "Wiki", "tiny", "/"},17 {"/repos", "Repositories | tiny", "Repositories", "tiny", "/"},18 {"/manage/people", "people | tiny", "People", "Manage", "/"},19 {"/social/thread/abc", "Social | tiny", "Social", "tiny", "/social"},20 {"/file/9999999999999999999999999999999999999999999999999999999999999999", "File | tiny", "File", "tiny", "/files"},21 {"/repos/alice/notes", "Repository | tiny", "Repository", "tiny", "/repos"},22 {"/profile", "Profile | tiny", "Profile", "tiny", "/account"},23 {"/sites", "sites | tiny", "Sites", "tiny", "/"},24 }25 for _, test := range tests {26 got := mobileHeader(PageData{Path: test.path, Title: test.title, Slug: "tiny"})27 if got.Title != test.wantTitle || got.Subtitle != test.wantSubtitle || got.BackURL != test.wantBack || got.BackLabel == "" && test.wantBack != "" {28 t.Errorf("mobileHeader(%q) = %+v, want title=%q subtitle=%q back=%q", test.path, got, test.wantTitle, test.wantSubtitle, test.wantBack)29 }30 }31}3233func TestMobileHeaderRoomAndThread(t *testing.T) {34 room := map[string]any{"room": map[string]any{"id": "general", "name": "The <General> Room", "picture": "https://cdn.example/room.png"}}35 got := mobileHeader(PageData{Path: "/rooms/general", Tab: "room", Event: room, URL: "https://relay.example"})36 if got.Title != "The <General> Room" || got.Subtitle != "Chat" || got.BackURL != "/chat" || got.RoomID != "general" || got.Picture == "" {37 t.Fatalf("room header = %+v", got)38 }39 root := strings.Repeat("a", 64)40 thread := mobileHeader(PageData{Path: "/rooms/general/thread/" + root, Tab: "thread", Event: map[string]any{41 "room": map[string]any{"id": "general", "name": "General"},42 "root": map[string]any{"id": root},43 }})44 if thread.Title != "Thread" || thread.Subtitle != "General" || thread.BackURL != "/rooms/general#msg-"+root || thread.BackLabel != "Back to room" {45 t.Fatalf("thread header = %+v", thread)46 }47}4849func TestThreadHeaderReturnsToCanonicalRoot(t *testing.T) {50 root, reply := strings.Repeat("a", 64), strings.Repeat("b", 64)51 header := mobileHeader(PageData{Path: "/rooms/general/thread/" + reply, Tab: "thread", Event: map[string]any{52 "room": map[string]any{"id": "general", "name": "General"},53 "root": map[string]any{"id": root},54 }})55 if header.BackURL != "/rooms/general#msg-"+root {56 t.Fatalf("thread back link points to a folded reply: %+v", header)57 }58}5960func TestMobileHeaderDirectAndHiddenData(t *testing.T) {61 peer := strings.Repeat("b", 64)62 got := mobileHeader(PageData{Path: "/chat/dm/" + peer, Tab: "direct", View: peer})63 if got.Title != "Direct chat" || got.Subtitle != "Private conversation" || got.PubKey != peer || got.BackURL != "/chat" {64 t.Fatalf("direct header = %+v", got)65 }66 private := mobileHeader(PageData{Path: "/rooms/secret", Tab: "room", Private: true, Event: map[string]any{"room": map[string]any{"name": "Hidden", "picture": "https://secret.example/p.png"}}})67 if private.Title != "Sign in" || private.Subtitle != "" || private.Picture != "" || private.RoomID != "" {68 t.Fatalf("private header leaked data: %+v", private)69 }70 privateDirect := mobileHeader(PageData{Path: "/chat/dm/" + peer, Tab: "direct", View: peer, Private: true, Slug: "tiny", Event: map[string]any{"pubkey": peer}})71 if privateDirect.Title != "Sign in" || privateDirect.PubKey != "" || privateDirect.RoomID != "" || privateDirect.Picture != "" {72 t.Fatalf("private direct header leaked data: %+v", privateDirect)73 }74 err := mobileHeader(PageData{Path: "/rooms/secret", Tab: "room", Error: "denied", Event: map[string]any{"room": map[string]any{"name": "Hidden"}}})75 if err.Title != "Chat" || err.RoomID != "" || err.BackURL != "/chat" || err.BackLabel != "Back to chats" {76 t.Fatalf("error header leaked data: %+v", err)77 }78 signin := mobileHeader(PageData{Path: "/manage/people", Tab: "signin", Title: "People | tiny", Slug: "tiny"})79 if signin.Title != "Sign in" || signin.Subtitle != "tiny" {80 t.Fatalf("signin header = %+v", signin)81 }82}8384func TestMobileHeaderUsesDeepLinkContext(t *testing.T) {85 article := mobileHeader(PageData{Path: "/social/" + strings.Repeat("a", 64), Tab: "social-thread", Slug: "tiny", Event: map[string]any{86 "post": map[string]any{"title": "A useful article"},87 }})88 if article.Title != "A useful article" || article.Subtitle != "Social" || article.BackURL != "/social" || article.BackLabel != "Back to social" {89 t.Fatalf("article header = %+v", article)90 }91 file := mobileHeader(PageData{Path: "/file/9999999999999999999999999999999999999999999999999999999999999999", Tab: "file", Slug: "tiny", Event: map[string]any{"name": "photo.mp4"}})92 if file.Title != "photo.mp4" || file.Subtitle != "Files" || file.BackURL != "/files" || file.BackLabel != "Back to files" {93 t.Fatalf("file header = %+v", file)94 }95 repo := mobileHeader(PageData{Path: "/repos/alice/seedmark", Tab: "repo", Slug: "tiny", Query: url.Values{"repo": []string{"seedmark"}, "view": []string{"issues"}}})96 if repo.Title != "seedmark" || repo.Subtitle != "issues" || repo.BackURL != "/repos" || repo.BackLabel != "Back to repositories" {97 t.Fatalf("repo header = %+v", repo)98 }99 wiki := mobileHeader(PageData{Path: "/wiki/release-notes", Tab: "wikipage", Slug: "tiny", Query: url.Values{"d": []string{"release-notes"}}, Event: map[string]any{"title": "Release notes"}})100 if wiki.Title != "Release notes" || wiki.Subtitle != "Wiki" || wiki.BackURL != "/wiki" {101 t.Fatalf("wiki header = %+v", wiki)102 }103 sites := mobileHeader(PageData{Path: "/files", Tab: "files", Slug: "tiny", Query: url.Values{"view": []string{"sites"}}})104 if sites.Title != "Sites" || sites.Subtitle != "Files" || sites.BackURL != "/" {105 t.Fatalf("sites header = %+v", sites)106 }107}108109func TestMobileHeaderManagementParent(t *testing.T) {110 got := mobileHeader(PageData{Path: "/manage/agents", Tab: "agents", Slug: "tiny"})111 if got.BackURL != "/manage/people" || got.BackLabel != "Back to manage" {112 t.Fatalf("management header = %+v", got)113 }114}115116func TestMobileHeaderSigninReturnValidation(t *testing.T) {117 valid := mobileHeader(PageData{Tab: "signin", Slug: "tiny", Base: "/r/demo", Query: url.Values{"next": []string{"/r/demo/social?q=hello"}}})118 if valid.BackURL != "/r/demo/social?q=hello" {119 t.Fatalf("valid signin return = %+v", valid)120 }121 for _, next := range []string{"/\\evil.test", "//evil.test", "/r/other/chat", "/r/demo/%5Cevil", "/signin"} {122 got := mobileHeader(PageData{Tab: "signin", Slug: "tiny", Base: "/r/demo", Query: url.Values{"next": []string{next}}})123 if got.BackURL != "" {124 t.Errorf("unsafe signin return %q accepted as %+v", next, got)125 }126 }127 connected := mobileHeader(PageData{Tab: "signin", Actor: strings.Repeat("a", 64), Slug: "tiny", Query: url.Values{"connect": []string{"1"}}})128 if connected.Title != "Connect signer" {129 t.Fatalf("connected signin header = %+v", connected)130 }131 account := mobileHeader(PageData{Tab: "signin", Actor: strings.Repeat("a", 64), Slug: "tiny"})132 if account.Title != "Account" {133 t.Fatalf("account signin header = %+v", account)134 }135}