| A face that opens someone 9bb81a1 nandi 17h ago | 1 | ## Who someone is, behind the nick. No network: every case here is a body in |
| 2 | ## and fields out, or a question about a nick. |
| 3 | |
| 4 | import std/[strutils, unicode] |
| 5 | import std/[json, unittest] |
| 6 | import frq/profile |
| 7 | |
| 8 | suite "isHandle": |
| 9 | test "a domain-shaped nick is a handle": |
| 10 | check isHandle("alice.bsky.social") |
| 11 | check isHandle("nandi.uk") |
| 12 | test "a bare nick is not": |
| 13 | # freeq gives an authenticated user their handle by default, while |
| 14 | # `sleek5209` is a guest with no profile. |
| 15 | check not isHandle("sleek5209") |
| 16 | check not isHandle("alice") |
| 17 | test "nor is something with no TLD": |
| 18 | check not isHandle("alice.") |
| 19 | check not isHandle("") |
| 20 | |
| 21 | suite "actorFor": |
| 22 | test "a DID wins, because it is the identity itself": |
| 23 | # A nick is whatever someone chose today. |
| 24 | check actorFor("did:plc:abc", "alice.bsky.social") == "did:plc:abc" |
| 25 | test "a handle-shaped nick stands in where there is no DID": |
| 26 | check actorFor("", "alice.bsky.social") == "alice.bsky.social" |
| 27 | test "a guest has nothing to look up": |
| 28 | check actorFor("", "sleek5209") == "" |
| 29 | |
| 30 | suite "thumbnailUrl": |
| 31 | test "asks the CDN for the size we paint": |
| 32 | # A 170KB portrait downloaded to draw at 24 points is the thing this |
| 33 | # avoids. |
| 34 | check thumbnailUrl("https://cdn.bsky.app/img/avatar/plain/did:plc:x/y@jpeg") == |
| 35 | "https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:x/y@png" |
| 36 | test "replaces an existing format suffix rather than appending": |
| 37 | check thumbnailUrl("https://cdn/x@jpeg").endsWith("@png") |
| 38 | check "@jpeg" notin thumbnailUrl("https://cdn/x@jpeg") |
| 39 | test "no picture is no URL": |
| 40 | check thumbnailUrl("") == "" |
| 41 | |
| 42 | suite "parseProfile": |
| 43 | let body = parseJson("""{ |
| 44 | "did": "did:plc:abc", "handle": "alice.bsky.social", |
| 45 | "displayName": " Alice ", "description": " hello ", |
| 46 | "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:abc/p@jpeg", |
| 47 | "followersCount": 12, "followsCount": 34, "postsCount": 56 }""") |
| 48 | |
| 49 | test "the fields the panel paints": |
| 50 | let p = parseProfile(body) |
| 51 | check p.status == psReady |
| 52 | check p.did == "did:plc:abc" |
| 53 | check p.handle == "alice.bsky.social" |
| 54 | check p.displayName == "Alice" # trimmed |
| 55 | check p.description == "hello" |
| 56 | check p.followers == 12 |
| 57 | check p.avatar.endsWith("@png") |
| 58 | |
| 59 | test "a body with nothing in it": |
| 60 | let p = parseProfile(parseJson("{}")) |
| 61 | check p.did == "" |
| 62 | check p.avatar == "" |
| 63 | |
| 64 | suite "statsLine": |
| 65 | test "all three": |
| 66 | let p = Profile(followers: 12, follows: 34, posts: 56) |
| 67 | check statsLine(p) == "12 followers · 34 following · 56 posts" |
| 68 | test "only what is known": |
| 69 | check statsLine(Profile(posts: 5)) == "5 posts" |
| 70 | test "none is no line at all": |
| 71 | check statsLine(Profile()) == "" |
| 72 | |
| 73 | suite "webUrl": |
| 74 | test "by handle where there is one": |
| 75 | check webUrl(Profile(handle: "alice.bsky.social", did: "did:plc:x")) == |
| 76 | "https://bsky.app/profile/alice.bsky.social" |
| 77 | test "by DID otherwise": |
| 78 | check webUrl(Profile(did: "did:plc:x")) == "https://bsky.app/profile/did:plc:x" |
| 79 | test "a leading @ is not part of a handle": |
| 80 | check webUrl(Profile(handle: "@alice.bsky.social")) == |
| 81 | "https://bsky.app/profile/alice.bsky.social" |
| 82 | test "nobody is no URL": |
| 83 | check webUrl(Profile()) == "" |
| 84 | |
| 85 | suite "truncate": |
| 86 | test "leaves a short bio alone, line breaks and all": |
| 87 | # The height of a multi-line bio is part of what it says. |
| 88 | check truncate("one\ntwo", 280) == "one\ntwo" |
| 89 | test "cuts a long one": |
| 90 | let got = truncate("x".repeat(400), 280) |
| 91 | check got.runeLen == 280 |
| 92 | check got.endsWith("…") |
| 93 | |
| 94 | test "cuts by character, not by byte": |
| 95 | let got = truncate("😀".repeat(400), 280) |
| 96 | check got.runeLen == 280 |
| 97 | check got.validateUtf8 == -1 |