| freeqsay: Nim port of the Clojure implementation fa53aa2 nandi 22h ago | 1 | import std/[json, os, sequtils, strutils, tables, unittest] |
| 2 | import ../src/freeqsay |
| 3 | |
| 4 | const didA = "did:plc:ewvi7nmzuoqusbcablsm7c4h" |
| 5 | |
| 6 | let fixtures = parseFile(currentSourcePath().parentDir / ".." / |
| 7 | "fixtures" / "avatar-conformance.json") |
| 8 | |
| 9 | proc stubGet(did: string): HttpGet = |
| 10 | ## Test hook standing in for the network: always answers with `did`. |
| 11 | result = proc (url: string): HttpResponse {.gcsafe.} = |
| 12 | HttpResponse(status: 200, body: %*{"did": did}) |
| 13 | |
| 14 | suite "sha256": |
| 15 | test "vectors": |
| 16 | check toHex(digest("")) == |
| 17 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" |
| 18 | check toHex(digest("abc")) == |
| 19 | "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" |
| 20 | # RFC 4231 test case 1 |
| 21 | check toHex(hmac(repeat(0x0b'u8, 20), toBytes("Hi There"))) == |
| 22 | "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" |
| 23 | |
| 24 | suite "avatar-v1 conformance": |
| 25 | test "fixture replays": |
| 26 | check fixtures.len > 0 |
| 27 | for f in fixtures: |
| 28 | let |
| 29 | did = f["did"].getStr |
| 30 | av = deriveAvatar(did) |
| 31 | checkpoint did |
| 32 | check f["canonical_seed"].getStr == toHex(canonicalSeed(did)) |
| 33 | for key, want in f["expected_traits"]: |
| 34 | check av.traits[key] == want.getStr |
| 35 | check f["sprite_hash"].getStr == spriteHash(av) |
| 36 | |
| 37 | suite "balloons": |
| 38 | test "wraps long lines": |
| 39 | let lines = wrapText("one two three four five", 10) |
| 40 | check lines.allIt(it.len <= 10) |
| 41 | check "one" in lines.join(" ") |
| 42 | |
| 43 | test "single-line speech balloon": |
| 44 | let b = makeBalloon("hi") |
| 45 | check "< hi" in b |
| 46 | let top = b.splitLines[0] |
| 47 | check top.startsWith(" ") and top[1 .. ^1].allIt(it == '_') |
| 48 | |
| 49 | test "multi-line balloon": |
| 50 | let b = makeBalloon("hello there friend", width = 6) |
| 51 | check "/ " in b |
| 52 | check "\\ " in b |
| 53 | |
| 54 | test "thought bubble": |
| 55 | check "( hmm )" in makeBalloon("hmm", think = true) |
| 56 | |
| 57 | suite "balloons preserve ANSI shape": |
| 58 | let |
| 59 | esc = "\x1b" |
| 60 | art = didToAnsi(didA, scale = 1) |
| 61 | lines = art.splitLines |
| 62 | |
| 63 | test "escape sequences cost no columns": |
| 64 | check visibleWidth(esc & "[38;2;1;2;3mabc" & esc & "[0m") == 3 |
| 65 | check lines.allIt(visibleWidth(it) == 16) |
| 66 | |
| 67 | test "art lines survive a balloon verbatim": |
| 68 | let |
| 69 | b = makeBalloon(art, width = 40) |
| 70 | body = b.splitLines[1 .. ^2] |
| 71 | # every row is padded to one width, so the right border lines up |
| 72 | let borders = body.mapIt(it.filterIt(it in {'|', '/', '\\'}).len) |
| 73 | check borders.deduplicate.len == 1 |
| 74 | check body.len == lines.len |
| 75 | for i, l in lines: |
| 76 | check l in body[i] |
| 77 | |
| 78 | test "indentation is not collapsed": |
| 79 | check wrapText(" indented", 40) == @[" indented"] |
| 80 | |
| 81 | suite "say": |
| 82 | test "balloon + character for handle + message": |
| 83 | let said = freeqsay("alice.test", "hello world", scale = 1, |
| 84 | resolveOpts = ResolveOpts(httpGet: stubGet(didA))) |
| 85 | check said.did == didA |
| 86 | check said.text == "hello world" |
| 87 | check "< hello world >" in said.output |
| 88 | check "\\" in said.output |
| 89 | check "\x1b[" in said.output |
| 90 | |
| 91 | test "raw DID needs no network": |
| 92 | check "< yo >" in freeqsay(didA, "yo", scale = 1).output |
| 93 | |
| 94 | suite "resolving": |
| 95 | test "handle normalization": |
| 96 | check normalizeHandle("@Jay.Bsky.Team") == "jay.bsky.team" |
| 97 | check isDid(didA) |
| 98 | check not isDid("jay.bsky.team") |
| 99 | |
| 100 | test "DIDs pass through without a request": |
| 101 | check resolveIdentity(didA).did == didA |
| 102 | |
| 103 | test "handles resolve through the endpoint": |
| 104 | var seen = "" |
| 105 | let get = proc (url: string): HttpResponse {.gcsafe.} = |
| 106 | {.cast(gcsafe).}: seen = url |
| 107 | HttpResponse(status: 200, body: %*{"did": didA}) |
| 108 | check resolveHandle("@example.bsky.social", ResolveOpts(httpGet: get)) == didA |
| 109 | check "handle=example.bsky.social" in seen |
| 110 | |
| 111 | test "a dead transport says so instead of blaming the handle": |
| 112 | let get = proc (url: string): HttpResponse {.gcsafe.} = |
| 113 | HttpResponse(status: 0, error: "no TLS here") |
| 114 | expect IOError: |
| 115 | discard resolveHandle("alice.test", ResolveOpts(httpGet: get)) |
| 116 | |
| 117 | suite "ansi": |
| 118 | test "truecolor escapes": |
| 119 | check "\x1b[" in didToAnsi(didA, scale = 1) |
| 120 | |
| 121 | test "handle->ansi matches did->ansi after resolve": |
| 122 | let id = resolveIdentity("alice.test", ResolveOpts(httpGet: stubGet(didA))) |
| 123 | check didToAnsi(didA, scale = 1) == didToAnsi(id.did, scale = 1) |
| 124 | |
| 125 | suite "png": |
| 126 | test "signature": |
| 127 | let bs = didToPng(didA, scale = 2) |
| 128 | check bs[0 .. 1] == @[137'u8, 80'u8] |
| 129 | check bs[12 .. 15] == toBytes("IHDR") |
| 130 | check bs[^4 .. ^1] == @[0xae'u8, 0x42, 0x60, 0x82] # IEND crc |