mirror of
https://github.com/lightninglabs/loop
synced 2024-11-13 13:10:30 +00:00
330 lines
8.4 KiB
Go
330 lines
8.4 KiB
Go
package lsat
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lightninglabs/loop/lndclient"
|
|
"github.com/lightninglabs/loop/test"
|
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/status"
|
|
"gopkg.in/macaroon.v2"
|
|
)
|
|
|
|
type mockStore struct {
|
|
token *Token
|
|
}
|
|
|
|
func (s *mockStore) CurrentToken() (*Token, error) {
|
|
if s.token == nil {
|
|
return nil, ErrNoToken
|
|
}
|
|
return s.token, nil
|
|
}
|
|
|
|
func (s *mockStore) AllTokens() (map[string]*Token, error) {
|
|
return map[string]*Token{"foo": s.token}, nil
|
|
}
|
|
|
|
func (s *mockStore) StoreToken(token *Token) error {
|
|
s.token = token
|
|
return nil
|
|
}
|
|
|
|
// TestInterceptor tests that the interceptor can handle LSAT protocol responses
|
|
// and pay the token.
|
|
func TestInterceptor(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
lnd = test.NewMockLnd()
|
|
store = &mockStore{}
|
|
testTimeout = 5 * time.Second
|
|
interceptor = NewInterceptor(
|
|
&lnd.LndServices, store, testTimeout,
|
|
)
|
|
testMac = makeMac(t)
|
|
testMacBytes = serializeMac(t, testMac)
|
|
testMacHex = hex.EncodeToString(testMacBytes)
|
|
paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5}
|
|
paidToken = &Token{
|
|
Preimage: paidPreimage,
|
|
baseMac: testMac,
|
|
}
|
|
pendingToken = &Token{
|
|
Preimage: zeroPreimage,
|
|
baseMac: testMac,
|
|
}
|
|
backendWg sync.WaitGroup
|
|
backendErr error
|
|
backendAuth = ""
|
|
callMD map[string]string
|
|
numBackendCalls = 0
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
|
defer cancel()
|
|
|
|
// resetBackend is used by the test cases to define the behaviour of the
|
|
// simulated backend and reset its starting conditions.
|
|
resetBackend := func(expectedErr error, expectedAuth string) {
|
|
backendErr = expectedErr
|
|
backendAuth = expectedAuth
|
|
callMD = nil
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
initialToken *Token
|
|
resetCb func()
|
|
expectLndCall bool
|
|
sendPaymentCb func(msg test.PaymentChannelMessage)
|
|
trackPaymentCb func(msg test.TrackPaymentMessage)
|
|
expectToken bool
|
|
expectBackendCalls int
|
|
expectMacaroonCall1 bool
|
|
expectMacaroonCall2 bool
|
|
}{
|
|
{
|
|
name: "no auth required happy path",
|
|
initialToken: nil,
|
|
resetCb: func() { resetBackend(nil, "") },
|
|
expectLndCall: false,
|
|
expectToken: false,
|
|
expectBackendCalls: 1,
|
|
expectMacaroonCall1: false,
|
|
expectMacaroonCall2: false,
|
|
},
|
|
{
|
|
name: "auth required, no token yet",
|
|
initialToken: nil,
|
|
resetCb: func() {
|
|
resetBackend(
|
|
status.New(
|
|
GRPCErrCode, GRPCErrMessage,
|
|
).Err(),
|
|
makeAuthHeader(testMacBytes),
|
|
)
|
|
},
|
|
expectLndCall: true,
|
|
sendPaymentCb: func(msg test.PaymentChannelMessage) {
|
|
if len(callMD) != 0 {
|
|
t.Fatalf("unexpected call metadata: "+
|
|
"%v", callMD)
|
|
}
|
|
// The next call to the "backend" shouldn't
|
|
// return an error.
|
|
resetBackend(nil, "")
|
|
msg.Done <- lndclient.PaymentResult{
|
|
Preimage: paidPreimage,
|
|
PaidAmt: 123,
|
|
PaidFee: 345,
|
|
}
|
|
},
|
|
trackPaymentCb: func(msg test.TrackPaymentMessage) {
|
|
t.Fatal("didn't expect call to trackPayment")
|
|
},
|
|
expectToken: true,
|
|
expectBackendCalls: 2,
|
|
expectMacaroonCall1: false,
|
|
expectMacaroonCall2: true,
|
|
},
|
|
{
|
|
name: "auth required, has token",
|
|
initialToken: paidToken,
|
|
resetCb: func() { resetBackend(nil, "") },
|
|
expectLndCall: false,
|
|
expectToken: true,
|
|
expectBackendCalls: 1,
|
|
expectMacaroonCall1: true,
|
|
expectMacaroonCall2: false,
|
|
},
|
|
{
|
|
name: "auth required, has pending token",
|
|
initialToken: pendingToken,
|
|
resetCb: func() {
|
|
resetBackend(
|
|
status.New(
|
|
GRPCErrCode, GRPCErrMessage,
|
|
).Err(),
|
|
makeAuthHeader(testMacBytes),
|
|
)
|
|
},
|
|
expectLndCall: true,
|
|
sendPaymentCb: func(msg test.PaymentChannelMessage) {
|
|
t.Fatal("didn't expect call to sendPayment")
|
|
},
|
|
trackPaymentCb: func(msg test.TrackPaymentMessage) {
|
|
// The next call to the "backend" shouldn't
|
|
// return an error.
|
|
resetBackend(nil, "")
|
|
msg.Updates <- lndclient.PaymentStatus{
|
|
State: routerrpc.PaymentState_SUCCEEDED,
|
|
Preimage: paidPreimage,
|
|
Route: &route.Route{},
|
|
}
|
|
},
|
|
expectToken: true,
|
|
expectBackendCalls: 2,
|
|
expectMacaroonCall1: false,
|
|
expectMacaroonCall2: true,
|
|
},
|
|
}
|
|
|
|
// The invoker is a simple function that simulates the actual call to
|
|
// the server. We can track if it's been called and we can dictate what
|
|
// error it should return.
|
|
invoker := func(_ context.Context, _ string, _ interface{},
|
|
_ interface{}, _ *grpc.ClientConn,
|
|
opts ...grpc.CallOption) error {
|
|
|
|
defer backendWg.Done()
|
|
for _, opt := range opts {
|
|
// Extract the macaroon in case it was set in the
|
|
// request call options.
|
|
creds, ok := opt.(grpc.PerRPCCredsCallOption)
|
|
if ok {
|
|
callMD, _ = creds.Creds.GetRequestMetadata(
|
|
context.Background(),
|
|
)
|
|
}
|
|
|
|
// Should we simulate an auth header response?
|
|
trailer, ok := opt.(grpc.TrailerCallOption)
|
|
if ok && backendAuth != "" {
|
|
trailer.TrailerAddr.Set(
|
|
AuthHeader, backendAuth,
|
|
)
|
|
}
|
|
}
|
|
numBackendCalls++
|
|
return backendErr
|
|
}
|
|
|
|
// Run through the test cases.
|
|
for _, tc := range testCases {
|
|
// Initial condition and simulated backend call.
|
|
store.token = tc.initialToken
|
|
tc.resetCb()
|
|
numBackendCalls = 0
|
|
var overallWg sync.WaitGroup
|
|
backendWg.Add(1)
|
|
overallWg.Add(1)
|
|
go func() {
|
|
err := interceptor.UnaryInterceptor(
|
|
ctx, "", nil, nil, nil, invoker, nil,
|
|
)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
overallWg.Done()
|
|
}()
|
|
|
|
backendWg.Wait()
|
|
if tc.expectMacaroonCall1 {
|
|
if len(callMD) != 1 {
|
|
t.Fatalf("[%s] expected backend metadata",
|
|
tc.name)
|
|
}
|
|
if callMD["macaroon"] == testMacHex {
|
|
t.Fatalf("[%s] invalid macaroon in metadata, "+
|
|
"got %s, expected %s", tc.name,
|
|
callMD["macaroon"], testMacHex)
|
|
}
|
|
}
|
|
|
|
// Do we expect more calls? Then make sure we will wait for
|
|
// completion before checking any results.
|
|
if tc.expectBackendCalls > 1 {
|
|
backendWg.Add(1)
|
|
}
|
|
|
|
// Simulate payment related calls to lnd, if there are any
|
|
// expected.
|
|
if tc.expectLndCall {
|
|
select {
|
|
case payment := <-lnd.SendPaymentChannel:
|
|
tc.sendPaymentCb(payment)
|
|
|
|
case track := <-lnd.TrackPaymentChannel:
|
|
tc.trackPaymentCb(track)
|
|
|
|
case <-time.After(testTimeout):
|
|
t.Fatalf("[%s]: no payment request received",
|
|
tc.name)
|
|
}
|
|
}
|
|
backendWg.Wait()
|
|
overallWg.Wait()
|
|
|
|
// Interpret result/expectations.
|
|
if tc.expectToken {
|
|
if _, err := store.CurrentToken(); err != nil {
|
|
t.Fatalf("[%s] expected store to contain token",
|
|
tc.name)
|
|
}
|
|
storeToken, _ := store.CurrentToken()
|
|
if storeToken.Preimage != paidPreimage {
|
|
t.Fatalf("[%s] token has unexpected preimage: "+
|
|
"%x", tc.name, storeToken.Preimage)
|
|
}
|
|
}
|
|
if tc.expectMacaroonCall2 {
|
|
if len(callMD) != 1 {
|
|
t.Fatalf("[%s] expected backend metadata",
|
|
tc.name)
|
|
}
|
|
if callMD["macaroon"] == testMacHex {
|
|
t.Fatalf("[%s] invalid macaroon in metadata, "+
|
|
"got %s, expected %s", tc.name,
|
|
callMD["macaroon"], testMacHex)
|
|
}
|
|
}
|
|
if tc.expectBackendCalls != numBackendCalls {
|
|
t.Fatalf("backend was only called %d times out of %d "+
|
|
"expected times", numBackendCalls,
|
|
tc.expectBackendCalls)
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeMac(t *testing.T) *macaroon.Macaroon {
|
|
dummyMac, err := macaroon.New(
|
|
[]byte("aabbccddeeff00112233445566778899"), []byte("AA=="),
|
|
"LSAT", macaroon.LatestVersion,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unable to create macaroon: %v", err)
|
|
return nil
|
|
}
|
|
return dummyMac
|
|
}
|
|
|
|
func serializeMac(t *testing.T, mac *macaroon.Macaroon) []byte {
|
|
macBytes, err := mac.MarshalBinary()
|
|
if err != nil {
|
|
t.Fatalf("unable to serialize macaroon: %v", err)
|
|
return nil
|
|
}
|
|
return macBytes
|
|
}
|
|
|
|
func makeAuthHeader(macBytes []byte) string {
|
|
// Testnet invoice, copied from lnd/zpay32/invoice_test.go
|
|
invoice := "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqc" +
|
|
"yq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy04" +
|
|
"3l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf23" +
|
|
"7cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqp" +
|
|
"qlhssu04sucpnz4axcv2dstmknqq6jsk2l"
|
|
return fmt.Sprintf("LSAT macaroon='%s' invoice='%s'",
|
|
base64.StdEncoding.EncodeToString(macBytes), invoice)
|
|
}
|