Skip to content

Commit 35b3533

Browse files
committed
test: remaining
1 parent a504b5f commit 35b3533

2 files changed

Lines changed: 213 additions & 0 deletions

File tree

pkg/api/api_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ type testServerOptions struct {
137137
ChequebookDisabled bool
138138
SwapDisabled bool
139139
Erc20ServiceNil bool
140+
// ServiceOut, when set, receives the constructed *api.Service so tests
141+
// can drive it directly (e.g. via a custom net.Listener) instead of
142+
// through the httptest.Server this function also sets up.
143+
ServiceOut **api.Service
140144
}
141145

142146
func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket.Conn, string, *chanStorer) {
@@ -251,6 +255,10 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket.
251255
s.EnableFullAPI()
252256
}
253257

258+
if o.ServiceOut != nil {
259+
*o.ServiceOut = s
260+
}
261+
254262
if o.DirectUpload {
255263
chanStore = newChanStore(o.Storer.PusherFeed())
256264
t.Cleanup(chanStore.stop)

pkg/api/gsoc_test.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@ import (
99
"context"
1010
"encoding/hex"
1111
"fmt"
12+
"net"
1213
"net/http"
1314
"net/url"
1415
"strings"
16+
"sync"
1517
"testing"
1618
"time"
1719

1820
"github.com/ethersphere/bee/v2/pkg/api"
1921
"github.com/ethersphere/bee/v2/pkg/cac"
2022
"github.com/ethersphere/bee/v2/pkg/crypto"
2123
"github.com/ethersphere/bee/v2/pkg/gsoc"
24+
"github.com/ethersphere/bee/v2/pkg/jsonhttp"
25+
"github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest"
2226
"github.com/ethersphere/bee/v2/pkg/log"
2327
mockbatchstore "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock"
2428
"github.com/ethersphere/bee/v2/pkg/soc"
@@ -243,6 +247,207 @@ func TestGsocWebsocketSocFieldsDeduplication(t *testing.T) {
243247
}
244248
}
245249

250+
// TestGsocWebsocketInvalidFieldsHeader verifies that an unknown field name in
251+
// the Swarm-Soc-Fields header is rejected with a 400 Bad Request before the
252+
// websocket upgrade is attempted.
253+
func TestGsocWebsocketInvalidFieldsHeader(t *testing.T) {
254+
t.Parallel()
255+
256+
var (
257+
id = make([]byte, 32)
258+
gsocSvc = gsoc.New(log.Noop)
259+
addrHex = hex.EncodeToString(id)
260+
batchStore = mockbatchstore.New()
261+
storer = mockstorer.New()
262+
)
263+
testutil.CleanupCloser(t, gsocSvc)
264+
265+
client, _, _, _ := newTestServer(t, testServerOptions{
266+
Gsoc: gsocSvc,
267+
Storer: storer,
268+
BatchStore: batchStore,
269+
Logger: log.Noop,
270+
})
271+
272+
jsonhttptest.Request(t, client, http.MethodGet, "/gsoc/subscribe/"+addrHex, http.StatusBadRequest,
273+
jsonhttptest.WithRequestHeader(api.SwarmSocFieldsHeader, "bogusfield"),
274+
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{
275+
Message: "invalid soc fields header",
276+
Code: http.StatusBadRequest,
277+
}),
278+
)
279+
}
280+
281+
// TestGsocWebsocketSlowConsumer verifies that when a subscriber cannot keep up
282+
// with incoming GSOC messages, the server closes the connection instead of
283+
// blocking indefinitely or racing on the underlying websocket connection.
284+
//
285+
// The connection is served over an in-memory net.Pipe, which is fully
286+
// synchronous (unbuffered): a write only completes once a matching read
287+
// consumes it. This makes the small dataC buffer overflow deterministically
288+
// as soon as the client stops reading, instead of depending on the size of
289+
// the OS's (possibly very large, auto-tuned) TCP socket buffers.
290+
func TestGsocWebsocketSlowConsumer(t *testing.T) {
291+
t.Parallel()
292+
293+
const messageCount = 10
294+
295+
var (
296+
id = make([]byte, 32)
297+
batchStore = mockbatchstore.New()
298+
storer = mockstorer.New()
299+
gsocSvc = gsoc.New(log.Noop)
300+
svc *api.Service
301+
)
302+
testutil.CleanupCloser(t, gsocSvc)
303+
304+
newTestServer(t, testServerOptions{
305+
Gsoc: gsocSvc,
306+
Storer: storer,
307+
BatchStore: batchStore,
308+
Logger: log.Noop,
309+
ServiceOut: &svc,
310+
})
311+
312+
privKey, err := crypto.GenerateSecp256k1Key()
313+
if err != nil {
314+
t.Fatal(err)
315+
}
316+
signer := crypto.NewDefaultSigner(privKey)
317+
owner, err := signer.EthereumAddress()
318+
if err != nil {
319+
t.Fatal(err)
320+
}
321+
chunkAddr, _ := soc.CreateAddress(id, owner.Bytes())
322+
323+
ln := newPipeListener()
324+
srv := &http.Server{Handler: svc}
325+
testutil.CleanupCloser(t, srv)
326+
go func() { _ = srv.Serve(ln) }()
327+
328+
clientConn, serverConn := net.Pipe()
329+
ln.offer(serverConn)
330+
331+
u := url.URL{Scheme: "ws", Host: "pipe", Path: "/gsoc/subscribe/" + hex.EncodeToString(chunkAddr.Bytes())}
332+
dialer := websocket.Dialer{
333+
NetDial: func(_, _ string) (net.Conn, error) { return clientConn, nil },
334+
}
335+
cl, _, err := dialer.Dial(u.String(), nil)
336+
if err != nil {
337+
t.Fatalf("client handshake: %v", err)
338+
}
339+
testutil.CleanupCloser(t, cl)
340+
341+
// never read from cl, so the dataC buffer (cap 2) fills up almost
342+
// immediately: the first message blocks the single writer goroutine
343+
// (nothing reads the pipe), and the next ones queue up and overflow.
344+
for i := range messageCount {
345+
payload := []byte{byte(i)}
346+
ch, _ := cac.New(payload)
347+
socCh := soc.New(id, ch)
348+
signedCh, _ := socCh.Sign(signer)
349+
socCh, _ = soc.FromChunk(signedCh)
350+
gsocSvc.Handle(socCh)
351+
}
352+
353+
if err := cl.SetReadDeadline(time.Now().Add(longTimeout)); err != nil {
354+
t.Fatal(err)
355+
}
356+
357+
// Drain whatever messages had already been handed to the (synchronous)
358+
// pipe before the overflow was detected; the connection must eventually
359+
// be closed instead of the server delivering every message regardless of
360+
// how far behind the consumer falls.
361+
var readErr error
362+
for i := 0; i < messageCount && readErr == nil; i++ {
363+
_, _, readErr = cl.ReadMessage()
364+
}
365+
if readErr == nil {
366+
t.Fatal("expected connection to be closed for a slow consumer")
367+
}
368+
}
369+
370+
// pipeListener is a net.Listener that hands out pre-established net.Conn
371+
// pairs, so an http.Server can be driven over an in-memory net.Pipe instead
372+
// of a real OS socket.
373+
type pipeListener struct {
374+
connCh chan net.Conn
375+
closed chan struct{}
376+
once sync.Once
377+
}
378+
379+
func newPipeListener() *pipeListener {
380+
return &pipeListener{
381+
connCh: make(chan net.Conn, 1),
382+
closed: make(chan struct{}),
383+
}
384+
}
385+
386+
func (l *pipeListener) offer(conn net.Conn) { l.connCh <- conn }
387+
388+
func (l *pipeListener) Accept() (net.Conn, error) {
389+
select {
390+
case c := <-l.connCh:
391+
return c, nil
392+
case <-l.closed:
393+
return nil, net.ErrClosed
394+
}
395+
}
396+
397+
func (l *pipeListener) Close() error {
398+
l.once.Do(func() { close(l.closed) })
399+
return nil
400+
}
401+
402+
func (l *pipeListener) Addr() net.Addr { return pipeAddr{} }
403+
404+
type pipeAddr struct{}
405+
406+
func (pipeAddr) Network() string { return "pipe" }
407+
func (pipeAddr) String() string { return "pipe" }
408+
409+
// TestGsocWebsocketMessageOrdering verifies that sequential Handle calls for
410+
// the same GSOC address are delivered to the subscriber in the same order.
411+
func TestGsocWebsocketMessageOrdering(t *testing.T) {
412+
t.Parallel()
413+
414+
const messageCount = 10
415+
416+
var (
417+
id = make([]byte, 32)
418+
g, cl, signer, _ = newGsocTest(t, id, 0)
419+
)
420+
421+
err := cl.SetReadDeadline(time.Now().Add(longTimeout))
422+
if err != nil {
423+
t.Fatal(err)
424+
}
425+
cl.SetReadLimit(swarm.ChunkSize)
426+
427+
payloads := make([][]byte, messageCount)
428+
for i := range payloads {
429+
payloads[i] = fmt.Appendf(nil, "message-%d", i)
430+
}
431+
432+
for _, payload := range payloads {
433+
ch, _ := cac.New(payload)
434+
socCh := soc.New(id, ch)
435+
signedCh, _ := socCh.Sign(signer)
436+
socCh, _ = soc.FromChunk(signedCh)
437+
g.Handle(socCh)
438+
}
439+
440+
for i, want := range payloads {
441+
_, got, err := cl.ReadMessage()
442+
if err != nil {
443+
t.Fatalf("message %d: %v", i, err)
444+
}
445+
if !bytes.Equal(got, want) {
446+
t.Fatalf("message %d: got %q, want %q", i, got, want)
447+
}
448+
}
449+
}
450+
246451
// TestGsocWebsocketCacheWrappedChunk verifies that the Swarm-Cache-Wrapped-Chunk
247452
// header causes the wrapped chunk to be stored in the cache so that it can be
248453
// resolved through the bytes endpoint.

0 commit comments

Comments
 (0)