| 1 | package tinyclient |
| 2 | |
| 3 | import ( |
| 4 | "net/url" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | func TestMobileHeaderGeneralRoutes(t *testing.T) { |
| 10 | tests := []struct { |
| 11 | path, title, wantTitle, wantSubtitle, wantBack string |
| 12 | }{ |
| 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" , "File | tiny" , "File" , "tiny" , "/files" }, |
| 21 | {"/repo" , "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 | } |
| 32 | |
| 33 | func 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 | } |
| 48 | |
| 49 | func 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 | } |
| 59 | |
| 60 | func 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 | } |
| 83 | |
| 84 | func 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" , 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: "/repo" , 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 | } |
| 108 | |
| 109 | func 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 | } |
| 115 | |
| 116 | func 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 | } |