muun-recovery/libwallet/lnurl/lnurl_test.go
2025-05-13 17:49:44 -03:00

772 lines
21 KiB
Go

package lnurl
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/fiatjaf/go-lnurl"
"github.com/lightningnetwork/lnd/lnwire"
)
func TestWithdraw(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestWithdrawWithCompatibilityTag(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdraw",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestDecodeError(t *testing.T) {
qr := "lightning:abcde"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrDecode {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusContacting {
t.Fatal("should not contact server")
}
})
}
func TestWrongTagError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/channelRequest", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
Tag: "channelRequest",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/channelRequest", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrWrongTag {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not create invoice")
}
})
}
func TestUnreachableError(t *testing.T) {
originalTimeout := httpClient.Timeout
httpClient.Timeout = 1 * time.Second
defer func() {
httpClient.Timeout = originalTimeout
}()
// LNURL QR pointing to a non-responding domain
qr := "LIGHTNING:LNURL1DP68GURN8GHJ7ARGD9EJUER0D4SKJM3WV3HK2UEWDEHHGTN90P5HXAPWV4UXZMTSD3JJUCM0D5LHXETRWFJHG0F3XGENGDGQ8EH52"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrUnreachable {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not create invoice")
}
})
}
func TestServiceError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusError,
Reason: "something something",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusReceiving {
t.Fatal("should not reach receiving status")
}
})
}
func TestInvalidResponseError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foobar"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrInvalidResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func TestUnsafeURLError(t *testing.T) {
qr, _ := encode("http://localhost/withdraw")
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, false, func(e *Event) {
if e.Code < 100 && e.Code != ErrUnsafeURL {
t.Fatalf("unexpected error code: %v", e.Code)
}
})
}
func TestWrongTagInQR(t *testing.T) {
// LNURL QR with a `login` tag value in its query params
qr := "lightning:lnurl1dp68gurn8ghj7mrww4exctt5dahkccn00qhxget8wfjk2um0veax2un09e3k7mf0w5lhgct884kx7emfdcnxkvfa8qexxc35vymnxcf5xumkxvfsv4snxwph8qunzv3hxesnyv3jvv6nyv3e8yuxzvnpv4skvepnxg6rwv34xqck2c3sxcerzdpnv56r2dss2vt96"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrWrongTag {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusContacting {
t.Fatal("should not contact server")
}
})
}
func TestOnionLinkNotSupported(t *testing.T) {
qr := "LNURL1DP68GUP69UHKVMM0VFSHYTN0DE5K7MSHXU8YD"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrTorNotSupported {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusContacting {
t.Fatal("should not contact server")
}
})
}
func TestExpiredCheck(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: "ERROR",
Reason: "something something Expired blabla",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrRequestExpired {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not create invoice")
}
})
}
func TestNoAvailableBalance(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 0,
MinWithdrawable: 0,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrNoAvailableBalance {
t.Fatalf("unexpected error code: %d", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatalf("should not create invoice")
}
})
}
func TestNoRouteCheck(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusError,
Reason: "Unable to pay LN Invoice: FAILURE_REASON_NO_ROUTE",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
return "12345", nil
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrNoRoute {
t.Fatalf("unexpected error code: %d", e.Code)
}
})
}
func TestExtraQueryParams(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete?foo=bar",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("foo") != "bar" {
t.Fatalf("Expected foo=bar in query params. Got URL: %v", r.URL.String())
}
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestStringlyTypedNumberFields(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&struct {
Response
Tag string `json:"tag"`
K1 string `json:"k1"`
Callback string `json:"callback"`
MaxWithdrawable string `json:"maxWithdrawable"`
MinWithdrawable string `json:"minWithdrawable"`
DefaultDescription string `json:"defaultDescription"`
}{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: "1000000",
MinWithdrawable: "0",
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestErrorContainsResponseBody(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
w.Write([]byte("this is a custom error response"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrInvalidResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if !strings.Contains(e.Message, "this is a custom error response") {
t.Fatalf("expected error message to contain response, got `%s`", e.Message)
}
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func TestErrorContainsResponseBodyForFinishRequest(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
w.Write([]byte("this is a custom error response"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
return "12345", nil
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrInvalidResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if !strings.Contains(e.Message, "this is a custom error response") {
t.Fatalf("expected error message to contain response, got `%s`", e.Message)
}
}
if e.Code == StatusReceiving {
t.Fatal("should not reach receiving status")
}
})
}
func TestForbidden(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(403)
w.Write([]byte("Forbidden"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrForbidden {
t.Fatalf("unexpected error code: %v", e.Code)
}
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func TestZebedee403MapsToCountryNotSupported(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(403)
w.Write([]byte("Forbidden"))
})
server := httptest.NewServer(mux)
defer server.Close()
// Super ugly hack to emulate that local endpoint is zebedee
zebedeeHost = "127.0.0.1"
t.Cleanup(func() {
zebedeeHost = zebedeeHostConst // after test reset to its original value
})
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrCountryNotSupported {
t.Fatalf("unexpected error code: %v", e.Code)
}
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func encode(url string) (string, error) {
return lnurl.LNURLEncode(url)
}
func TestWithdrawResponse_Validate(t *testing.T) {
type fields struct {
Response Response
Tag string
K1 string
Callback string
MaxWithdrawable stringOrNumber
MinWithdrawable stringOrNumber
DefaultDescription string
}
errorResponse := func(reason string) fields {
return fields{
Response: Response{
Status: StatusError,
Reason: reason,
},
}
}
tests := []struct {
name string
fields fields
want int
}{
{
"invalid tag",
fields{Tag: "blebidy"},
ErrWrongTag,
},
{
"negative withdraw",
fields{MaxWithdrawable: -1, Tag: "withdraw"},
ErrNoAvailableBalance,
},
{
"valid",
fields{
Response: Response{
Status: StatusOK,
},
Tag: "withdraw",
MaxWithdrawable: 10,
},
ErrNone,
},
{
"already being processed",
errorResponse("This Withdrawal Request is already being processed by another wallet"),
ErrAlreadyUsed,
},
{
"can only be processed only once",
errorResponse("This Withdrawal Request can only be processed once"),
ErrAlreadyUsed,
},
{
"withdraw is spent",
errorResponse("Withdraw is spent"),
ErrAlreadyUsed,
},
{
"withdraw link is empty",
errorResponse("Withdraw link is empty"),
ErrAlreadyUsed,
},
{
"has already been used",
errorResponse("This LNURL has already been used"),
ErrAlreadyUsed,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wr := &WithdrawResponse{
Response: tt.fields.Response,
Tag: tt.fields.Tag,
K1: tt.fields.K1,
Callback: tt.fields.Callback,
MaxWithdrawable: tt.fields.MaxWithdrawable,
MinWithdrawable: tt.fields.MinWithdrawable,
DefaultDescription: tt.fields.DefaultDescription,
}
got, _ := wr.Validate()
if got != tt.want {
t.Errorf("Validate() got = %v, want %v", got, tt.want)
}
})
}
}
func TestValidate(t *testing.T) {
type args struct {
qr string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "plain",
args: args{"LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "lightning scheme",
args: args{"lightning:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "HTTP fallback scheme",
args: args{"https://example.com/?lightning=LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun scheme",
args: args{"muun:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun scheme with double slashes",
args: args{"muun://LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "lightning scheme with double slashes",
args: args{"lightning://LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun + lightning schemes",
args: args{"muun:lightning:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun + lightning schemes with double slashes",
args: args{"muun://lightning:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Validate(tt.args.qr); got != tt.want {
t.Errorf("Validate() = %v, want %v", got, tt.want)
}
})
}
}