Repository navigation
Expand file tree
/
Copy pathswap_server_client_test.go
More file actions
242 lines (200 loc) · 6.16 KB
/
Copy pathswap_server_client_test.go
File metadata and controls
242 lines (200 loc) · 6.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
package loop
import (
"context"
"errors"
"testing"
"time"
"github.com/lightninglabs/aperture/l402"
looptest "github.com/lightninglabs/loop/test"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
"gopkg.in/macaroon.v2"
)
// TestL402ClientInterceptorConcurrentPaidCalls verifies that calls using an
// existing paid token reach the transport concurrently.
func TestL402ClientInterceptorConcurrentPaidCalls(t *testing.T) {
t.Parallel()
paidMacaroon, err := macaroon.New(
[]byte("root key"), []byte("token id"), "test",
macaroon.LatestVersion,
)
require.NoError(t, err)
interceptor := &l402ClientInterceptor{
callTimeout: time.Minute,
loadPaidMacaroon: func() (*macaroon.Macaroon, bool, error) {
return paidMacaroon, true, nil
},
fallbackUnary: func(context.Context, string, any, any,
*grpc.ClientConn, grpc.UnaryInvoker,
...grpc.CallOption) error {
return errors.New("unexpected L402 fallback")
},
}
callStarted := make(chan int, 2)
releaseCalls := make(chan struct{})
invoker := func(_ context.Context, _ string, req, _ any,
_ *grpc.ClientConn, opts ...grpc.CallOption) error {
// A paid call must carry the original option plus the L402
// credential before it reaches the transport.
if len(opts) != 2 {
return errors.New("paid call has unexpected " +
"call options")
}
callStarted <- req.(int)
<-releaseCalls
return nil
}
callErr := make(chan error, 2)
for id := 1; id <= 2; id++ {
go func() {
callErr <- interceptor.UnaryInterceptor(
t.Context(), "test", id, nil, nil, invoker,
grpc.WaitForReady(true),
)
}()
}
// Both calls must arrive while the first remains blocked. This is the
// property the Aperture interceptor's unconditional mutex prevented.
started := map[int]bool{
receiveOrTimeout(t, callStarted): true,
receiveOrTimeout(t, callStarted): true,
}
require.Equal(t, map[int]bool{1: true, 2: true}, started)
close(releaseCalls)
require.NoError(t, receiveOrTimeout(t, callErr))
require.NoError(t, receiveOrTimeout(t, callErr))
}
// TestL402ClientInterceptorPaymentChallengeFallback verifies that a challenge
// on the concurrent path is retried through serialized token handling.
func TestL402ClientInterceptorPaymentChallengeFallback(t *testing.T) {
t.Parallel()
paidMacaroon, err := macaroon.New(
[]byte("root key"), []byte("token id"), "test",
macaroon.LatestVersion,
)
require.NoError(t, err)
fallbackCalled := make(chan struct{}, 1)
interceptor := &l402ClientInterceptor{
callTimeout: time.Minute,
loadPaidMacaroon: func() (*macaroon.Macaroon, bool, error) {
return paidMacaroon, true, nil
},
fallbackUnary: func(context.Context, string, any, any,
*grpc.ClientConn, grpc.UnaryInvoker,
...grpc.CallOption) error {
fallbackCalled <- struct{}{}
return nil
},
}
invoker := func(context.Context, string, any, any,
*grpc.ClientConn, ...grpc.CallOption) error {
// Simulate the server rejecting the cached paid token so the
// payment-aware fallback owns any acquisition or retry.
return status.Error(l402.GRPCErrCode, l402.GRPCErrMessage)
}
err = interceptor.UnaryInterceptor(
t.Context(), "test", nil, nil, nil, invoker,
)
require.NoError(t, err)
receiveOrTimeout(t, fallbackCalled)
}
// TestL402ClientInterceptorCurrentTokenFallback verifies that token states
// which cannot use the concurrent paid path enter serialized token handling.
func TestL402ClientInterceptorCurrentTokenFallback(t *testing.T) {
t.Parallel()
storeErr := errors.New("token store failed")
testCases := []struct {
name string
token *l402.Token
tokenErr error
}{
{
name: "no token",
tokenErr: l402.ErrNoToken,
},
{
name: "pending token",
token: &l402.Token{},
},
{
name: "nil token",
},
{
name: "store error",
tokenErr: storeErr,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
fallbackCalled := false
interceptor := &l402ClientInterceptor{
tokenStore: &staticL402Store{
token: testCase.token,
err: testCase.tokenErr,
},
fallbackUnary: func(context.Context, string,
any, any, *grpc.ClientConn,
grpc.UnaryInvoker,
...grpc.CallOption) error {
// Reaching this callback proves the
// unusable token did not enter the
// concurrent transport path.
fallbackCalled = true
return nil
},
}
interceptor.loadPaidMacaroon =
interceptor.currentPaidMacaroon
invoker := func(context.Context, string, any, any,
*grpc.ClientConn, ...grpc.CallOption) error {
return errors.New("unexpected concurrent call")
}
err := interceptor.UnaryInterceptor(
t.Context(), "test", nil, nil, nil, invoker,
)
require.NoError(t, err)
require.True(t, fallbackCalled)
})
}
}
// receiveOrTimeout returns the next channel value or fails after one second.
func receiveOrTimeout[T any](t *testing.T, values <-chan T) T {
t.Helper()
select {
case value := <-values:
return value
case <-time.After(time.Second):
var zero T
t.Fatal("timed out waiting for test value")
return zero
}
}
// staticL402Store returns a configured token result for interceptor tests.
type staticL402Store struct {
// Store supplies methods unused by the token-loading tests.
l402.Store
// token is returned by CurrentToken.
token *l402.Token
// err is returned by CurrentToken.
err error
}
// CurrentToken returns the token result configured by the test.
func (s *staticL402Store) CurrentToken() (*l402.Token, error) {
return s.token, s.err
}
// TestParseServerPubKey ensures that parseServerPubKey accepts a valid
// compressed public key and rejects keys with an invalid length or contents.
func TestParseServerPubKey(t *testing.T) {
t.Parallel()
_, pubKey := looptest.CreateKey(1)
pubKeyBytes := pubKey.SerializeCompressed()
parsedKey, err := parseServerPubKey("test key", pubKeyBytes)
require.NoError(t, err)
require.Equal(t, pubKeyBytes, parsedKey[:])
_, err = parseServerPubKey("test key", pubKeyBytes[:32])
require.ErrorContains(t, err, "invalid test key length")
invalidKey := make([]byte, 33)
_, err = parseServerPubKey("test key", invalidKey)
require.ErrorContains(t, err, "invalid test key")
}