package lnurl_test import ( "strings" "testing" "github.com/drjones/quantum-arcade/pkg/lnurl" ) // The BIP-173 test vectors. An implementation that passes these produces // strings other wallets will accept; one that does not produces codes that // simply fail to scan, with no useful error for the player. func TestBIP173ValidVectors(t *testing.T) { valid := []string{ "A12UEL5L", "a12uel5l", "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", // The 90-character vector, built rather than transcribed: getting the // run length wrong by hand produces a checksum failure that looks like // an implementation bug. "11" + strings.Repeat("q", 82) + "c8247j", "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", "?1ezyfcl", } for _, v := range valid { if _, _, err := lnurl.Decode(v); err != nil { t.Errorf("valid vector %q rejected: %v", v, err) } } } func TestBIP173InvalidVectors(t *testing.T) { invalid := map[string]string{ "A12UEL5X": "bad checksum", "pzry9x0s0muk": "no separator", "1pzry9x0s0muk": "empty hrp", "x1b4n0q5v": "invalid character", "li1dgmt3": "too short", "A1G7SGD8": "bad checksum", "10a06t8": "empty hrp", "1qzzfhee": "empty hrp", "abc1rzg": "too short", "in1muywd": "bad checksum", "A12Uel5l": "mixed case", } for v, why := range invalid { if _, _, err := lnurl.Decode(v); err == nil { t.Errorf("invalid vector %q (%s) was accepted", v, why) } } } func TestRoundTrip(t *testing.T) { cases := []string{ "https://arcade.lan/lnurl/withdraw?k1=abc123", "http://10.0.0.5:8080/lnurl/withdraw?k1=" + strings.Repeat("f", 64), "https://example.com/", } for _, url := range cases { encoded, err := lnurl.EncodeURL(url) if err != nil { t.Fatalf("encoding %q: %v", url, err) } // Wallets receive these uppercase, so decoding must handle that. decoded, err := lnurl.DecodeURL(encoded) if err != nil { t.Fatalf("decoding %q: %v", encoded, err) } if decoded != url { t.Fatalf("round trip changed the URL: %q -> %q", url, decoded) } } } // LNURL strings are uppercase so the QR encodes in alphanumeric mode, which is // substantially denser than byte mode and keeps the code scannable on a phone. func TestEncodedLNURLIsUppercase(t *testing.T) { s, err := lnurl.EncodeURL("https://arcade.lan/lnurl/withdraw?k1=deadbeef") if err != nil { t.Fatal(err) } if s != strings.ToUpper(s) { t.Fatalf("LNURL is not uppercase: %q", s) } if !strings.HasPrefix(s, "LNURL1") { t.Fatalf("LNURL lacks the expected prefix: %q", s) } } // A tampered character must fail the checksum rather than decode to a // different URL — otherwise a corrupted scan could point a wallet somewhere // unintended. func TestTamperingIsDetected(t *testing.T) { original := "https://arcade.lan/lnurl/withdraw?k1=abc123" encoded, err := lnurl.EncodeURL(original) if err != nil { t.Fatal(err) } detected := 0 attempts := 0 for i := 6; i < len(encoded); i++ { for _, sub := range "QPZRY9X8" { if rune(encoded[i]) == sub { continue } attempts++ tampered := encoded[:i] + string(sub) + encoded[i+1:] if _, err := lnurl.DecodeURL(tampered); err != nil { detected++ } } } if attempts == 0 { t.Fatal("no tampering attempts were made") } // The checksum catches all single-character substitutions by design. if detected != attempts { t.Fatalf("only %d of %d single-character changes were detected", detected, attempts) } } func TestWrongPrefixRejected(t *testing.T) { // A valid bech32 string that is not an LNURL must not be accepted as one. other, err := lnurl.Encode("lnbc", []byte("not an lnurl")) if err != nil { t.Fatal(err) } if _, err := lnurl.DecodeURL(other); err == nil { t.Fatal("a non-LNURL bech32 string was accepted as an LNURL") } }