From e5a7b0a6f3c25651c589bfe191871d1a860b32fc Mon Sep 17 00:00:00 2001 From: nugaon Date: Mon, 8 Jun 2026 13:39:39 +0200 Subject: [PATCH 01/33] feat: add SOC fields headers and wrapped chunk caching Add `Swarm-Soc-Fields` header to allow clients to request specific SOC fields (address, recoveredpubkey, identifier, signature, wrappedaddress, span, payload) in GSOC WebSocket messages. Add `Swarm-Cache-Wrapped-Chunk` header to enable caching of wrapped chunks on the node. Update GSOC handler to pass full SOC object instead of just payload, enabling access to all chunk properties. Adjust WebSocket buffer sizes to accommodate maximum SOC fields message size. --- pkg/api/api.go | 3 + pkg/api/gsoc.go | 149 +++++++++++++++++++++++++++++++++++++++++++---- pkg/gsoc/gsoc.go | 7 ++- pkg/soc/soc.go | 4 ++ 4 files changed, 150 insertions(+), 13 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 63a04c390ff..426cbcb1837 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -96,6 +96,8 @@ const ( SwarmActTimestampHeader = "Swarm-Act-Timestamp" SwarmActPublisherHeader = "Swarm-Act-Publisher" SwarmActHistoryAddressHeader = "Swarm-Act-History-Address" + SwarmSocFieldsHeader = "Swarm-Soc-Fields" + SwarmCacheWrappedChunkHeader = "Swarm-Cache-Wrapped-Chunk" ImmutableHeader = "Immutable" GasPriceHeader = "Gas-Price" @@ -607,6 +609,7 @@ func (s *Service) corsHandler(h http.Handler) http.Handler { SwarmRedundancyStrategyHeader, SwarmRedundancyFallbackModeHeader, SwarmChunkRetrievalTimeoutHeader, SwarmLookAheadBufferSizeHeader, SwarmFeedIndexHeader, SwarmFeedIndexNextHeader, SwarmSocSignatureHeader, SwarmOnlyRootChunk, GasPriceHeader, GasLimitHeader, ImmutableHeader, SwarmActHeader, SwarmActTimestampHeader, SwarmActPublisherHeader, SwarmActHistoryAddressHeader, + SwarmSocFieldsHeader, SwarmCacheWrappedChunkHeader, } allowedHeadersStr := strings.Join(allowedHeaders, ", ") diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index 60d048ffdc0..f879603a71f 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -5,15 +5,108 @@ package api import ( + "bytes" + "context" + "fmt" "net/http" + "slices" + "strings" "time" "github.com/ethersphere/bee/v2/pkg/jsonhttp" + "github.com/ethersphere/bee/v2/pkg/soc" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/gorilla/mux" "github.com/gorilla/websocket" ) +// SOC field identifiers that can be requested through the SwarmSocFieldsHeader +// to be serialized and channeled on every incoming GSOC chunk. +const ( + socFieldAddress = "address" + socFieldRecoveredPubKey = "recoveredpubkey" + socFieldIdentifier = "identifier" + socFieldSignature = "signature" + socFieldWrappedAddress = "wrappedaddress" + socFieldSpan = "span" + socFieldPayload = "payload" +) + +var validSocFields = []string{ + socFieldAddress, + socFieldRecoveredPubKey, + socFieldIdentifier, + socFieldSignature, + socFieldWrappedAddress, + socFieldSpan, + socFieldPayload, +} + +// maxSocFieldsSize is the maximum size of a serialized SOC fields message when +// every field is requested: the whole single owner chunk (identifier + +// signature + span + payload, i.e. SocMaxChunkSize) plus the derived metadata +// fields that are not part of the chunk on the wire (soc address, recovered +// public key and wrapped chunk address). +const maxSocFieldsSize = swarm.SocMaxChunkSize + + swarm.HashSize + // soc address + soc.OwnerPubKeySize + // recovered public key + swarm.HashSize // wrapped chunk address + +// parseSocFields parses the SwarmSocFieldsHeader value into a list of SOC field +// identifiers. When the header is empty it defaults to the payload field only, +// which preserves backward compatibility. +func parseSocFields(header string) ([]string, error) { + if strings.TrimSpace(header) == "" { + return []string{socFieldPayload}, nil + } + + parts := strings.Split(header, ",") + fields := make([]string, 0, len(parts)) + for _, p := range parts { + f := strings.ToLower(strings.TrimSpace(p)) + if f == "" { + continue + } + if !slices.Contains(validSocFields, f) { + return nil, fmt.Errorf("unknown soc field: %q", p) + } + fields = append(fields, f) + } + if len(fields) == 0 { + return []string{socFieldPayload}, nil + } + return fields, nil +} + +// socFieldsBytes serializes the requested SOC fields in the same order as they +// were provided in the header. +func socFieldsBytes(c *soc.SOC, fields []string) ([]byte, error) { + buf := bytes.NewBuffer(nil) + for _, f := range fields { + switch f { + case socFieldAddress: + addr, err := c.Address() + if err != nil { + return nil, fmt.Errorf("soc address: %w", err) + } + buf.Write(addr.Bytes()) + case socFieldRecoveredPubKey: + buf.Write(c.OwnerPubKey()) + case socFieldIdentifier: + buf.Write(c.ID()) + case socFieldSignature: + buf.Write(c.Signature()) + case socFieldWrappedAddress: + buf.Write(c.WrappedChunk().Address().Bytes()) + case socFieldSpan: + buf.Write(c.WrappedChunk().Data()[:swarm.SpanSize]) + case socFieldPayload: + buf.Write(c.WrappedChunk().Data()[swarm.SpanSize:]) + } + } + return buf.Bytes(), nil +} + func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { logger := s.logger.WithName("gsoc_subscribe").Build() @@ -26,9 +119,31 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { return } + headers := struct { + SocFields string `map:"Swarm-Soc-Fields"` + CacheWrappedChunk bool `map:"Swarm-Cache-Wrapped-Chunk"` + }{} + if response := s.mapStructure(r.Header, &headers); response != nil { + response("invalid header params", logger, w) + return + } + + fields, err := parseSocFields(headers.SocFields) + if err != nil { + logger.Debug("invalid soc fields header", "error", err) + logger.Error(nil, "invalid soc fields header") + jsonhttp.BadRequest(w, "invalid soc fields header") + return + } + upgrader := websocket.Upgrader{ - ReadBufferSize: swarm.ChunkSize, - WriteBufferSize: swarm.ChunkSize, + ReadBufferSize: swarm.SocMaxChunkSize, + // WriteBufferSize is only an I/O buffer hint; it does not cap the + // message size. The serialized output can be the whole single owner + // chunk plus the derived metadata fields (soc address, recovered public + // key, wrapped chunk address), so size it to that maximum to avoid split + // writes. + WriteBufferSize: maxSocFieldsSize, CheckOrigin: s.checkOrigin, } @@ -41,25 +156,39 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { } s.wsWg.Add(1) - go s.gsocListeningWs(conn, paths.Address) + go s.gsocListeningWs(conn, paths.Address, fields, headers.CacheWrappedChunk) } -func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address) { +func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address, fields []string, cacheWrappedChunk bool) { defer s.wsWg.Done() var ( - dataC = make(chan []byte) - gone = make(chan struct{}) - ticker = time.NewTicker(s.WsPingPeriod) - err error + dataC = make(chan []byte) + gone = make(chan struct{}) + ticker = time.NewTicker(s.WsPingPeriod) + ctx, cancel = context.WithCancel(context.Background()) // for storing cached chunks + err error ) defer func() { + cancel() ticker.Stop() _ = conn.Close() }() - cleanup := s.gsoc.Subscribe(socAddress, func(m []byte) { + cleanup := s.gsoc.Subscribe(socAddress, func(c *soc.SOC) { + if cacheWrappedChunk { + if err := s.storer.Cache().Put(ctx, c.WrappedChunk()); err != nil { + s.logger.Debug("gsoc ws: cache wrapped chunk failed", "error", err) + } + } + + b, err := socFieldsBytes(c, fields) + if err != nil { + s.logger.Debug("gsoc ws: serialize soc fields failed", "error", err) + return + } + select { - case dataC <- m: + case dataC <- b: case <-gone: return case <-s.quit: diff --git a/pkg/gsoc/gsoc.go b/pkg/gsoc/gsoc.go index 41e4f54ac2c..343bf24aeaf 100644 --- a/pkg/gsoc/gsoc.go +++ b/pkg/gsoc/gsoc.go @@ -13,8 +13,9 @@ import ( ) // Handler defines code to be executed upon reception of a GSOC sub message. -// it is used as a parameter definition. -type Handler func([]byte) +// it is used as a parameter definition. It receives the recovered single owner +// chunk so the consumer has access to all of its properties. +type Handler func(*soc.SOC) type Listener interface { Subscribe(address swarm.Address, handler Handler) (cleanup func()) @@ -73,7 +74,7 @@ func (l *listener) Handle(c *soc.SOC) { for _, hh := range h { go func(hh Handler) { - hh(c.WrappedChunk().Data()[swarm.SpanSize:]) + hh(c) }(*hh) } } diff --git a/pkg/soc/soc.go b/pkg/soc/soc.go index 28ade83eeb8..09a6cd81c2e 100644 --- a/pkg/soc/soc.go +++ b/pkg/soc/soc.go @@ -20,6 +20,10 @@ var ( errWrongChunkSize = errors.New("soc: chunk length is less than minimum") ) +// OwnerPubKeySize is the byte length of a compressed secp256k1 public key, +// as returned by crypto.EncodeSecp256k1PublicKey. +const OwnerPubKeySize = 33 + // ID is a SOC identifier type ID []byte From 7bbf91e115db5d4be3aaa5bbbb9b130f7168b988 Mon Sep 17 00:00:00 2001 From: nugaon Date: Mon, 8 Jun 2026 13:41:44 +0200 Subject: [PATCH 02/33] test: add SOC fields and wrapped chunk caching Add tests for `Swarm-Soc-Fields` header to verify requesting specific SOC fields (identifier, wrappedAddress, payload) and full wrapped chunk data (span + payload). Add test for `Swarm-Cache-Wrapped-Chunk` header to verify wrapped chunks are cached and retrievable. Update test helpers to support custom headers and return storer instance. Update gsoc handler signature to accept full SOC object instead of payload bytes. --- pkg/api/gsoc_test.go | 121 +++++++++++++++++++++++++++++++++++++++++- pkg/gsoc/gsoc_test.go | 6 +-- 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index cf3161a9f03..6ab7e6daf9c 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -5,13 +5,17 @@ package api_test import ( + "bytes" + "context" "encoding/hex" "fmt" + "net/http" "net/url" "strings" "testing" "time" + "github.com/ethersphere/bee/v2/pkg/api" "github.com/ethersphere/bee/v2/pkg/cac" "github.com/ethersphere/bee/v2/pkg/crypto" "github.com/ethersphere/bee/v2/pkg/gsoc" @@ -134,7 +138,121 @@ func TestGsocPong(t *testing.T) { } } +// TestGsocWebsocketWrappedChunkData verifies that the Swarm-Soc-Fields header +// allows requesting the whole wrapped chunk data (span + payload). +func TestGsocWebsocketWrappedChunkData(t *testing.T) { + t.Parallel() + + var ( + id = make([]byte, 32) + headers = http.Header{api.SwarmSocFieldsHeader: []string{"span,payload"}} + g, cl, signer, _, _ = newGsocTestWithOpts(t, id, 0, headers) + respC = make(chan error, 1) + payload = []byte("The most dangerous phrase in the language is: ‘We've always done it this way.’") + ) + + err := cl.SetReadDeadline(time.Now().Add(longTimeout)) + if err != nil { + t.Fatal(err) + } + cl.SetReadLimit(swarm.ChunkSize) + + ch, _ := cac.New(payload) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + g.Handle(socCh) + + // span (8 bytes) + payload == full wrapped chunk data + go expectMessage(t, cl, respC, ch.Data()) + if err := <-respC; err != nil { + t.Fatal(err) + } +} + +// TestGsocWebsocketSocFields verifies that multiple SOC fields are serialized in +// the order they are provided in the Swarm-Soc-Fields header. +func TestGsocWebsocketSocFields(t *testing.T) { + t.Parallel() + + var ( + id = make([]byte, 32) + headers = http.Header{api.SwarmSocFieldsHeader: []string{"identifier,wrappedAddress,payload"}} + g, cl, signer, _, _ = newGsocTestWithOpts(t, id, 0, headers) + respC = make(chan error, 1) + payload = []byte("The future is already here — it's just not evenly distributed.") + ) + + err := cl.SetReadDeadline(time.Now().Add(longTimeout)) + if err != nil { + t.Fatal(err) + } + cl.SetReadLimit(swarm.ChunkSize) + + ch, _ := cac.New(payload) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + g.Handle(socCh) + + expected := make([]byte, 0) + expected = append(expected, id...) + expected = append(expected, ch.Address().Bytes()...) + expected = append(expected, payload...) + + go expectMessage(t, cl, respC, expected) + if err := <-respC; err != nil { + t.Fatal(err) + } +} + +// TestGsocWebsocketCacheWrappedChunk verifies that the Swarm-Cache-Wrapped-Chunk +// header causes the wrapped chunk to be stored in the cache so that it can be +// resolved through the bytes endpoint. +func TestGsocWebsocketCacheWrappedChunk(t *testing.T) { + t.Parallel() + + var ( + id = make([]byte, 32) + headers = http.Header{api.SwarmCacheWrappedChunkHeader: []string{"true"}} + g, cl, signer, _, storer = newGsocTestWithOpts(t, id, 0, headers) + respC = make(chan error, 1) + payload = []byte("If you don't like change, you're going to like irrelevance even less.") + ) + + err := cl.SetReadDeadline(time.Now().Add(longTimeout)) + if err != nil { + t.Fatal(err) + } + cl.SetReadLimit(swarm.ChunkSize) + + ch, _ := cac.New(payload) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + g.Handle(socCh) + + go expectMessage(t, cl, respC, payload) + if err := <-respC; err != nil { + t.Fatal(err) + } + + got, err := storer.ChunkStore().Get(context.Background(), ch.Address()) + if err != nil { + t.Fatalf("wrapped chunk not cached: %v", err) + } + if !bytes.Equal(got.Data(), ch.Data()) { + t.Fatal("cached wrapped chunk data mismatch") + } +} + func newGsocTest(t *testing.T, socId []byte, pingPeriod time.Duration) (gsoc.Listener, *websocket.Conn, crypto.Signer, string) { + t.Helper() + g, cl, signer, listener, _ := newGsocTestWithOpts(t, socId, pingPeriod, nil) + return g, cl, signer, listener +} + +func newGsocTestWithOpts(t *testing.T, socId []byte, pingPeriod time.Duration, headers http.Header) (gsoc.Listener, *websocket.Conn, crypto.Signer, string, api.Storer) { t.Helper() if pingPeriod == 0 { pingPeriod = 10 * time.Second @@ -161,11 +279,12 @@ func newGsocTest(t *testing.T, socId []byte, pingPeriod time.Duration) (gsoc.Lis _, cl, listener, _ := newTestServer(t, testServerOptions{ Gsoc: gsoc, WsPath: fmt.Sprintf("/gsoc/subscribe/%s", hex.EncodeToString(chunkAddr.Bytes())), + WsHeaders: headers, Storer: storer, BatchStore: batchStore, Logger: log.Noop, WsPingPeriod: pingPeriod, }) - return gsoc, cl, signer, listener + return gsoc, cl, signer, listener, storer } diff --git a/pkg/gsoc/gsoc_test.go b/pkg/gsoc/gsoc_test.go index dc49b0809a8..9beb892da51 100644 --- a/pkg/gsoc/gsoc_test.go +++ b/pkg/gsoc/gsoc_test.go @@ -37,17 +37,17 @@ func TestRegister(t *testing.T) { address1, _ = soc.CreateAddress(socId1, owner.Bytes()) address2, _ = soc.CreateAddress(socId2, owner.Bytes()) - h1 = func(m []byte) { + h1 = func(*soc.SOC) { h1Calls++ msgChan <- struct{}{} } - h2 = func(m []byte) { + h2 = func(*soc.SOC) { h2Calls++ msgChan <- struct{}{} } - h3 = func(m []byte) { + h3 = func(*soc.SOC) { h3Calls++ msgChan <- struct{}{} } From 9f63b76fa4a4ac27e8c6030a41ce292072ff11ac Mon Sep 17 00:00:00 2001 From: nugaon Date: Mon, 8 Jun 2026 13:44:33 +0200 Subject: [PATCH 03/33] docs: openapi with minor version bump --- openapi/Swarm.yaml | 12 ++++++++++-- openapi/SwarmCommon.yaml | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/openapi/Swarm.yaml b/openapi/Swarm.yaml index c320410b4ee..df362559b06 100644 --- a/openapi/Swarm.yaml +++ b/openapi/Swarm.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: - version: 8.1.1 + version: 8.2.0 title: Bee API description: "API endpoints for interacting with the Swarm network, supporting file operations, messaging, and node management" @@ -971,9 +971,17 @@ paths: $ref: "SwarmCommon.yaml#/components/schemas/SwarmAddress" required: true description: "Single Owner Chunk address (which may have multiple payloads)" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmSocFieldsParameter" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmCacheWrappedChunkParameter" responses: "200": - description: Establishes a WebSocket subscription for incoming messages on the Single Owner Chunk address + description: > + Establishes a WebSocket subscription for incoming messages on the + Single Owner Chunk address. Each message is the binary serialization + of the Single Owner Chunk fields requested through the + swarm-soc-fields header (defaults to the wrapped chunk payload). + "400": + $ref: "SwarmCommon.yaml#/components/responses/400" "500": $ref: "SwarmCommon.yaml#/components/responses/500" default: diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index ffcbbac3b8e..7ef301980f5 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -1144,6 +1144,29 @@ components: required: false description: Associate upload with an existing Tag UID + SwarmSocFieldsParameter: + in: header + name: swarm-soc-fields + schema: + type: string + required: false + description: > + Comma separated list of Single Owner Chunk fields to be serialized and + channeled on every incoming GSOC message, in the given order. Allowed + values are: address, recoveredPubKey, identifier, signature, + wrappedAddress, span, payload. When omitted it defaults to "payload". + + SwarmCacheWrappedChunkParameter: + in: header + name: swarm-cache-wrapped-chunk + schema: + type: boolean + required: false + description: > + Indicates whether the wrapped chunk of every incoming GSOC message should + be cached locally so that it can be resolved through the bytes endpoint + (useful when the single owner chunk wraps a root chunk larger than 4KB). + SwarmPinParameter: in: header name: swarm-pin From 8b4d996353abcaed386434acab512ac0eef9de02 Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 9 Jun 2026 10:06:27 +0200 Subject: [PATCH 04/33] fix: linting --- pkg/api/gsoc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 6ab7e6daf9c..f20e6fe782d 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -195,7 +195,7 @@ func TestGsocWebsocketSocFields(t *testing.T) { socCh, _ = soc.FromChunk(signedCh) g.Handle(socCh) - expected := make([]byte, 0) + expected := make([]byte, 0, len(id)+swarm.HashSize+len(payload)) expected = append(expected, id...) expected = append(expected, ch.Address().Bytes()...) expected = append(expected, payload...) From d38b6e897f3a7635016750a3cb06ac076e00089b Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 9 Jun 2026 14:17:44 +0200 Subject: [PATCH 05/33] refactor: warning log instead of debug --- pkg/api/gsoc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index f879603a71f..e3da611850f 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -183,7 +183,7 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address b, err := socFieldsBytes(c, fields) if err != nil { - s.logger.Debug("gsoc ws: serialize soc fields failed", "error", err) + s.logger.Warning("gsoc ws: serialize soc fields failed", "error", err) return } From e4116839d079d744602d3996e77cfbdada32fadf Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 9 Jun 2026 16:59:03 +0200 Subject: [PATCH 06/33] refactor: increase msg buffer to 2 and slow connection handling --- pkg/api/gsoc.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index e3da611850f..047ca5bb5ef 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -163,7 +163,7 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address defer s.wsWg.Done() var ( - dataC = make(chan []byte) + dataC = make(chan []byte, 2) // small buffer to decouple producer/consumer gone = make(chan struct{}) ticker = time.NewTicker(s.WsPingPeriod) ctx, cancel = context.WithCancel(context.Background()) // for storing cached chunks @@ -193,6 +193,13 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address return case <-s.quit: return + default: + s.logger.Warning("gsoc ws: slow consumer, closing connection") + _ = conn.WriteControl(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.ClosePolicyViolation, "slow consumer"), + time.Now().Add(writeDeadline)) + _ = conn.Close() + return } }) From 5ef413af7205f0323b37a52c8f444ad9f59202e4 Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 10 Jun 2026 11:03:20 +0200 Subject: [PATCH 07/33] docs: openapi random access desctiption --- openapi/SwarmCommon.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index 7ef301980f5..7803e1ff646 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -1155,6 +1155,8 @@ components: channeled on every incoming GSOC message, in the given order. Allowed values are: address, recoveredPubKey, identifier, signature, wrappedAddress, span, payload. When omitted it defaults to "payload". + In order to have random access on the response bytes define payload + as the last field in the list since it has variable length. SwarmCacheWrappedChunkParameter: in: header From 54df2e32d4c861ea2c8406525ab5054385da3d79 Mon Sep 17 00:00:00 2001 From: nugaon Date: Thu, 13 Aug 2026 10:14:48 +0200 Subject: [PATCH 08/33] docs: default payload --- openapi/SwarmCommon.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index 7803e1ff646..ef7fd9b9390 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -1149,6 +1149,7 @@ components: name: swarm-soc-fields schema: type: string + default: "payload" required: false description: > Comma separated list of Single Owner Chunk fields to be serialized and From a9921e3d73e6dcb74fa1097cef5768296a4b235d Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 19 Aug 2026 16:14:59 +0200 Subject: [PATCH 09/33] fix: race issue on close --- pkg/api/gsoc.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index 047ca5bb5ef..b1b4e8bf51d 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -11,6 +11,7 @@ import ( "net/http" "slices" "strings" + "sync" "time" "github.com/ethersphere/bee/v2/pkg/jsonhttp" @@ -165,6 +166,8 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address var ( dataC = make(chan []byte, 2) // small buffer to decouple producer/consumer gone = make(chan struct{}) + slow = make(chan struct{}) + slowOnce sync.Once ticker = time.NewTicker(s.WsPingPeriod) ctx, cancel = context.WithCancel(context.Background()) // for storing cached chunks err error @@ -190,16 +193,14 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address select { case dataC <- b: case <-gone: - return + case <-slow: case <-s.quit: - return default: + // The connection writer is single-threaded in the main loop below; + // only signal it here instead of writing/closing the conn from this + // callback goroutine, which can run concurrently with the writer. s.logger.Warning("gsoc ws: slow consumer, closing connection") - _ = conn.WriteControl(websocket.CloseMessage, - websocket.FormatCloseMessage(websocket.ClosePolicyViolation, "slow consumer"), - time.Now().Add(writeDeadline)) - _ = conn.Close() - return + slowOnce.Do(func() { close(slow) }) } }) @@ -241,6 +242,16 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address case <-gone: // client gone return + case <-slow: + err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) + if err != nil { + s.logger.Debug("gsoc ws: set write deadline failed", "error", err) + return + } + _ = conn.WriteControl(websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.ClosePolicyViolation, "slow consumer"), + time.Now().Add(writeDeadline)) + return case <-ticker.C: err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) if err != nil { From be9f2acea76867a983fbe0b502116044eb64e96a Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 19 Aug 2026 16:18:43 +0200 Subject: [PATCH 10/33] fix: deduplication values in the header values --- pkg/api/gsoc.go | 11 +++++++++-- pkg/api/gsoc_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index b1b4e8bf51d..c35d58aaa11 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -55,14 +55,17 @@ const maxSocFieldsSize = swarm.SocMaxChunkSize + // parseSocFields parses the SwarmSocFieldsHeader value into a list of SOC field // identifiers. When the header is empty it defaults to the payload field only, -// which preserves backward compatibility. +// which preserves backward compatibility. Duplicate fields are dropped, keeping +// the first occurrence, so the returned slice never exceeds len(validSocFields) +// entries regardless of how many times a field is repeated in the header. func parseSocFields(header string) ([]string, error) { if strings.TrimSpace(header) == "" { return []string{socFieldPayload}, nil } + seen := make(map[string]bool, len(validSocFields)) parts := strings.Split(header, ",") - fields := make([]string, 0, len(parts)) + fields := make([]string, 0, len(validSocFields)) for _, p := range parts { f := strings.ToLower(strings.TrimSpace(p)) if f == "" { @@ -71,6 +74,10 @@ func parseSocFields(header string) ([]string, error) { if !slices.Contains(validSocFields, f) { return nil, fmt.Errorf("unknown soc field: %q", p) } + if seen[f] { + continue + } + seen[f] = true fields = append(fields, f) } if len(fields) == 0 { diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index f20e6fe782d..1a9b7ac6bd6 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -206,6 +206,43 @@ func TestGsocWebsocketSocFields(t *testing.T) { } } +// TestGsocWebsocketSocFieldsDeduplication verifies that repeated field names in +// the Swarm-Soc-Fields header are de-duplicated, keeping only the first +// occurrence, instead of serializing the same field multiple times. +func TestGsocWebsocketSocFieldsDeduplication(t *testing.T) { + t.Parallel() + + var ( + id = make([]byte, 32) + headers = http.Header{api.SwarmSocFieldsHeader: []string{"payload,payload,identifier,payload,identifier"}} + g, cl, signer, _, _ = newGsocTestWithOpts(t, id, 0, headers) + respC = make(chan error, 1) + payload = []byte("Simplicity is the ultimate sophistication.") + ) + + err := cl.SetReadDeadline(time.Now().Add(longTimeout)) + if err != nil { + t.Fatal(err) + } + cl.SetReadLimit(swarm.ChunkSize) + + ch, _ := cac.New(payload) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + g.Handle(socCh) + + // each requested field must appear exactly once, in first-occurrence order + expected := make([]byte, 0, len(payload)+len(id)) + expected = append(expected, payload...) + expected = append(expected, id...) + + go expectMessage(t, cl, respC, expected) + if err := <-respC; err != nil { + t.Fatal(err) + } +} + // TestGsocWebsocketCacheWrappedChunk verifies that the Swarm-Cache-Wrapped-Chunk // header causes the wrapped chunk to be stored in the cache so that it can be // resolved through the bytes endpoint. From a504b5f0141e890922c935ce68cbdcc3ef12e0ac Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 19 Aug 2026 16:25:27 +0200 Subject: [PATCH 11/33] fix: context cancellation --- pkg/api/gsoc.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index c35d58aaa11..fe569982c39 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -171,22 +171,23 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address defer s.wsWg.Done() var ( - dataC = make(chan []byte, 2) // small buffer to decouple producer/consumer - gone = make(chan struct{}) - slow = make(chan struct{}) - slowOnce sync.Once - ticker = time.NewTicker(s.WsPingPeriod) - ctx, cancel = context.WithCancel(context.Background()) // for storing cached chunks - err error + dataC = make(chan []byte, 2) // small buffer to decouple producer/consumer + gone = make(chan struct{}) + slow = make(chan struct{}) + slowOnce sync.Once + ticker = time.NewTicker(s.WsPingPeriod) + err error ) defer func() { - cancel() ticker.Stop() _ = conn.Close() }() cleanup := s.gsoc.Subscribe(socAddress, func(c *soc.SOC) { if cacheWrappedChunk { - if err := s.storer.Cache().Put(ctx, c.WrappedChunk()); err != nil { + // Caching is a node-local side effect independent of this + // subscriber's connection, so it must not be aborted just + // because the websocket closes mid-write. + if err := s.storer.Cache().Put(context.Background(), c.WrappedChunk()); err != nil { s.logger.Debug("gsoc ws: cache wrapped chunk failed", "error", err) } } From 35b3533b38263f2354c811e5cf50ded6277ea51a Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 19 Aug 2026 17:08:33 +0200 Subject: [PATCH 12/33] test: remaining --- pkg/api/api_test.go | 8 ++ pkg/api/gsoc_test.go | 205 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 23782bcbc6b..c7376d6bbee 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -137,6 +137,10 @@ type testServerOptions struct { ChequebookDisabled bool SwapDisabled bool Erc20ServiceNil bool + // ServiceOut, when set, receives the constructed *api.Service so tests + // can drive it directly (e.g. via a custom net.Listener) instead of + // through the httptest.Server this function also sets up. + ServiceOut **api.Service } 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. s.EnableFullAPI() } + if o.ServiceOut != nil { + *o.ServiceOut = s + } + if o.DirectUpload { chanStore = newChanStore(o.Storer.PusherFeed()) t.Cleanup(chanStore.stop) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 1a9b7ac6bd6..64b926eef69 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -9,9 +9,11 @@ import ( "context" "encoding/hex" "fmt" + "net" "net/http" "net/url" "strings" + "sync" "testing" "time" @@ -19,6 +21,8 @@ import ( "github.com/ethersphere/bee/v2/pkg/cac" "github.com/ethersphere/bee/v2/pkg/crypto" "github.com/ethersphere/bee/v2/pkg/gsoc" + "github.com/ethersphere/bee/v2/pkg/jsonhttp" + "github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest" "github.com/ethersphere/bee/v2/pkg/log" mockbatchstore "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock" "github.com/ethersphere/bee/v2/pkg/soc" @@ -243,6 +247,207 @@ func TestGsocWebsocketSocFieldsDeduplication(t *testing.T) { } } +// TestGsocWebsocketInvalidFieldsHeader verifies that an unknown field name in +// the Swarm-Soc-Fields header is rejected with a 400 Bad Request before the +// websocket upgrade is attempted. +func TestGsocWebsocketInvalidFieldsHeader(t *testing.T) { + t.Parallel() + + var ( + id = make([]byte, 32) + gsocSvc = gsoc.New(log.Noop) + addrHex = hex.EncodeToString(id) + batchStore = mockbatchstore.New() + storer = mockstorer.New() + ) + testutil.CleanupCloser(t, gsocSvc) + + client, _, _, _ := newTestServer(t, testServerOptions{ + Gsoc: gsocSvc, + Storer: storer, + BatchStore: batchStore, + Logger: log.Noop, + }) + + jsonhttptest.Request(t, client, http.MethodGet, "/gsoc/subscribe/"+addrHex, http.StatusBadRequest, + jsonhttptest.WithRequestHeader(api.SwarmSocFieldsHeader, "bogusfield"), + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ + Message: "invalid soc fields header", + Code: http.StatusBadRequest, + }), + ) +} + +// TestGsocWebsocketSlowConsumer verifies that when a subscriber cannot keep up +// with incoming GSOC messages, the server closes the connection instead of +// blocking indefinitely or racing on the underlying websocket connection. +// +// The connection is served over an in-memory net.Pipe, which is fully +// synchronous (unbuffered): a write only completes once a matching read +// consumes it. This makes the small dataC buffer overflow deterministically +// as soon as the client stops reading, instead of depending on the size of +// the OS's (possibly very large, auto-tuned) TCP socket buffers. +func TestGsocWebsocketSlowConsumer(t *testing.T) { + t.Parallel() + + const messageCount = 10 + + var ( + id = make([]byte, 32) + batchStore = mockbatchstore.New() + storer = mockstorer.New() + gsocSvc = gsoc.New(log.Noop) + svc *api.Service + ) + testutil.CleanupCloser(t, gsocSvc) + + newTestServer(t, testServerOptions{ + Gsoc: gsocSvc, + Storer: storer, + BatchStore: batchStore, + Logger: log.Noop, + ServiceOut: &svc, + }) + + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + t.Fatal(err) + } + signer := crypto.NewDefaultSigner(privKey) + owner, err := signer.EthereumAddress() + if err != nil { + t.Fatal(err) + } + chunkAddr, _ := soc.CreateAddress(id, owner.Bytes()) + + ln := newPipeListener() + srv := &http.Server{Handler: svc} + testutil.CleanupCloser(t, srv) + go func() { _ = srv.Serve(ln) }() + + clientConn, serverConn := net.Pipe() + ln.offer(serverConn) + + u := url.URL{Scheme: "ws", Host: "pipe", Path: "/gsoc/subscribe/" + hex.EncodeToString(chunkAddr.Bytes())} + dialer := websocket.Dialer{ + NetDial: func(_, _ string) (net.Conn, error) { return clientConn, nil }, + } + cl, _, err := dialer.Dial(u.String(), nil) + if err != nil { + t.Fatalf("client handshake: %v", err) + } + testutil.CleanupCloser(t, cl) + + // never read from cl, so the dataC buffer (cap 2) fills up almost + // immediately: the first message blocks the single writer goroutine + // (nothing reads the pipe), and the next ones queue up and overflow. + for i := range messageCount { + payload := []byte{byte(i)} + ch, _ := cac.New(payload) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + gsocSvc.Handle(socCh) + } + + if err := cl.SetReadDeadline(time.Now().Add(longTimeout)); err != nil { + t.Fatal(err) + } + + // Drain whatever messages had already been handed to the (synchronous) + // pipe before the overflow was detected; the connection must eventually + // be closed instead of the server delivering every message regardless of + // how far behind the consumer falls. + var readErr error + for i := 0; i < messageCount && readErr == nil; i++ { + _, _, readErr = cl.ReadMessage() + } + if readErr == nil { + t.Fatal("expected connection to be closed for a slow consumer") + } +} + +// pipeListener is a net.Listener that hands out pre-established net.Conn +// pairs, so an http.Server can be driven over an in-memory net.Pipe instead +// of a real OS socket. +type pipeListener struct { + connCh chan net.Conn + closed chan struct{} + once sync.Once +} + +func newPipeListener() *pipeListener { + return &pipeListener{ + connCh: make(chan net.Conn, 1), + closed: make(chan struct{}), + } +} + +func (l *pipeListener) offer(conn net.Conn) { l.connCh <- conn } + +func (l *pipeListener) Accept() (net.Conn, error) { + select { + case c := <-l.connCh: + return c, nil + case <-l.closed: + return nil, net.ErrClosed + } +} + +func (l *pipeListener) Close() error { + l.once.Do(func() { close(l.closed) }) + return nil +} + +func (l *pipeListener) Addr() net.Addr { return pipeAddr{} } + +type pipeAddr struct{} + +func (pipeAddr) Network() string { return "pipe" } +func (pipeAddr) String() string { return "pipe" } + +// TestGsocWebsocketMessageOrdering verifies that sequential Handle calls for +// the same GSOC address are delivered to the subscriber in the same order. +func TestGsocWebsocketMessageOrdering(t *testing.T) { + t.Parallel() + + const messageCount = 10 + + var ( + id = make([]byte, 32) + g, cl, signer, _ = newGsocTest(t, id, 0) + ) + + err := cl.SetReadDeadline(time.Now().Add(longTimeout)) + if err != nil { + t.Fatal(err) + } + cl.SetReadLimit(swarm.ChunkSize) + + payloads := make([][]byte, messageCount) + for i := range payloads { + payloads[i] = fmt.Appendf(nil, "message-%d", i) + } + + for _, payload := range payloads { + ch, _ := cac.New(payload) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + g.Handle(socCh) + } + + for i, want := range payloads { + _, got, err := cl.ReadMessage() + if err != nil { + t.Fatalf("message %d: %v", i, err) + } + if !bytes.Equal(got, want) { + t.Fatalf("message %d: got %q, want %q", i, got, want) + } + } +} + // TestGsocWebsocketCacheWrappedChunk verifies that the Swarm-Cache-Wrapped-Chunk // header causes the wrapped chunk to be stored in the cache so that it can be // resolved through the bytes endpoint. From 125e62e45ccb4907e43102eba3d8b51fabd4fcd5 Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 2 Sep 2026 17:15:43 +0200 Subject: [PATCH 13/33] fix: handle gsoc messages sequentially gsoc.Handle spawned a goroutine per subscriber handler, so message delivery order to a subscriber was not guaranteed. Call handlers synchronously in registration order instead. --- pkg/gsoc/gsoc.go | 4 +--- pkg/gsoc/gsoc_test.go | 39 +++------------------------------------ 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/pkg/gsoc/gsoc.go b/pkg/gsoc/gsoc.go index 343bf24aeaf..f4ad79a6ccc 100644 --- a/pkg/gsoc/gsoc.go +++ b/pkg/gsoc/gsoc.go @@ -73,9 +73,7 @@ func (l *listener) Handle(c *soc.SOC) { l.logger.Debug("new incoming GSOC message", "GSOC Address", addr, "wrapped chunk address", c.WrappedChunk().Address()) for _, hh := range h { - go func(hh Handler) { - hh(c) - }(*hh) + (*hh)(c) } } diff --git a/pkg/gsoc/gsoc_test.go b/pkg/gsoc/gsoc_test.go index 9beb892da51..2ad61fff16d 100644 --- a/pkg/gsoc/gsoc_test.go +++ b/pkg/gsoc/gsoc_test.go @@ -6,7 +6,6 @@ package gsoc_test import ( "testing" - "time" "github.com/ethersphere/bee/v2/pkg/cac" "github.com/ethersphere/bee/v2/pkg/crypto" @@ -25,7 +24,6 @@ func TestRegister(t *testing.T) { h1Calls = 0 h2Calls = 0 h3Calls = 0 - msgChan = make(chan struct{}) payload1 = []byte("Hello there!") payload2 = []byte("General Kenobi. You are a bold one. Kill him!") @@ -37,20 +35,9 @@ func TestRegister(t *testing.T) { address1, _ = soc.CreateAddress(socId1, owner.Bytes()) address2, _ = soc.CreateAddress(socId2, owner.Bytes()) - h1 = func(*soc.SOC) { - h1Calls++ - msgChan <- struct{}{} - } - - h2 = func(*soc.SOC) { - h2Calls++ - msgChan <- struct{}{} - } - - h3 = func(*soc.SOC) { - h3Calls++ - msgChan <- struct{}{} - } + h1 = func(*soc.SOC) { h1Calls++ } + h2 = func(*soc.SOC) { h2Calls++ } + h3 = func(*soc.SOC) { h3Calls++ } ) _ = g.Subscribe(address1, h1) _ = g.Subscribe(address2, h2) @@ -68,8 +55,6 @@ func TestRegister(t *testing.T) { // trigger soc upload on address1, check that only h1 is called g.Handle(socCh1) - waitHandlerCallback(t, &msgChan, 1) - ensureCalls(t, &h1Calls, 1) ensureCalls(t, &h2Calls, 0) @@ -78,8 +63,6 @@ func TestRegister(t *testing.T) { g.Handle(socCh1) - waitHandlerCallback(t, &msgChan, 2) - ensureCalls(t, &h1Calls, 2) ensureCalls(t, &h2Calls, 0) ensureCalls(t, &h3Calls, 1) @@ -88,16 +71,12 @@ func TestRegister(t *testing.T) { g.Handle(socCh1) - waitHandlerCallback(t, &msgChan, 1) - ensureCalls(t, &h1Calls, 3) ensureCalls(t, &h2Calls, 0) ensureCalls(t, &h3Calls, 1) g.Handle(socCh2) - waitHandlerCallback(t, &msgChan, 1) - ensureCalls(t, &h1Calls, 3) ensureCalls(t, &h2Calls, 1) ensureCalls(t, &h3Calls, 1) @@ -110,15 +89,3 @@ func ensureCalls(t *testing.T, calls *int, exp int) { t.Fatalf("expected %d calls, found %d", exp, *calls) } } - -func waitHandlerCallback(t *testing.T, msgChan *chan struct{}, count int) { - t.Helper() - - for range count { - select { - case <-*msgChan: - case <-time.After(1 * time.Second): - t.Fatal("reached timeout while waiting for handler message") - } - } -} From 5e38e0557b225d664b932c330b551f4bcf91a82d Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 15 Sep 2026 09:53:16 +0200 Subject: [PATCH 14/33] refactor: remove slow client handling --- pkg/api/gsoc.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index fe569982c39..3c89d44b794 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -11,7 +11,6 @@ import ( "net/http" "slices" "strings" - "sync" "time" "github.com/ethersphere/bee/v2/pkg/jsonhttp" @@ -171,12 +170,11 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address defer s.wsWg.Done() var ( - dataC = make(chan []byte, 2) // small buffer to decouple producer/consumer - gone = make(chan struct{}) - slow = make(chan struct{}) - slowOnce sync.Once - ticker = time.NewTicker(s.WsPingPeriod) - err error + dataC = make(chan []byte) + gone = make(chan struct{}) + slow = make(chan struct{}) + ticker = time.NewTicker(s.WsPingPeriod) + err error ) defer func() { ticker.Stop() @@ -204,11 +202,7 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address case <-slow: case <-s.quit: default: - // The connection writer is single-threaded in the main loop below; - // only signal it here instead of writing/closing the conn from this - // callback goroutine, which can run concurrently with the writer. - s.logger.Warning("gsoc ws: slow consumer, closing connection") - slowOnce.Do(func() { close(slow) }) + s.logger.Warning("gsoc ws: slow consumer, messages piled up.") } }) From 8c0ba37b63586db9fc344d3d69eae4154bf4e9d1 Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 15 Sep 2026 10:30:58 +0200 Subject: [PATCH 15/33] refactor: remove slow consumer handling --- pkg/api/gsoc.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index 3c89d44b794..b831d47799d 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -172,7 +172,6 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address var ( dataC = make(chan []byte) gone = make(chan struct{}) - slow = make(chan struct{}) ticker = time.NewTicker(s.WsPingPeriod) err error ) @@ -199,10 +198,7 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address select { case dataC <- b: case <-gone: - case <-slow: case <-s.quit: - default: - s.logger.Warning("gsoc ws: slow consumer, messages piled up.") } }) @@ -244,16 +240,6 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address case <-gone: // client gone return - case <-slow: - err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) - if err != nil { - s.logger.Debug("gsoc ws: set write deadline failed", "error", err) - return - } - _ = conn.WriteControl(websocket.CloseMessage, - websocket.FormatCloseMessage(websocket.ClosePolicyViolation, "slow consumer"), - time.Now().Add(writeDeadline)) - return case <-ticker.C: err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) if err != nil { From 9f03baa6d6a4b56ed7a1410a79d79773232abff7 Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 15 Sep 2026 12:09:53 +0200 Subject: [PATCH 16/33] refactor: implement message queuing for slow consumers --- pkg/api/gsoc.go | 57 +++++++++++++++++++++++++++++++------------- pkg/api/gsoc_test.go | 46 +++++++++++++++++++---------------- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index b831d47799d..a7c03378219 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -11,6 +11,7 @@ import ( "net/http" "slices" "strings" + "sync" "time" "github.com/ethersphere/bee/v2/pkg/jsonhttp" @@ -170,10 +171,12 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address defer s.wsWg.Done() var ( - dataC = make(chan []byte) - gone = make(chan struct{}) - ticker = time.NewTicker(s.WsPingPeriod) - err error + queueMu sync.Mutex + queue [][]byte + wake = make(chan struct{}, 1) + gone = make(chan struct{}) + ticker = time.NewTicker(s.WsPingPeriod) + err error ) defer func() { ticker.Stop() @@ -195,10 +198,16 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address return } + queueMu.Lock() + queue = append(queue, b) + queueMu.Unlock() + + // Non-blocking: the writer only needs to know there is something to + // drain, not one notification per message, so a full wake channel + // means it is already going to pick this up. select { - case dataC <- b: - case <-gone: - case <-s.quit: + case wake <- struct{}{}: + default: } }) @@ -212,17 +221,31 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address for { select { - case b := <-dataC: - err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) - if err != nil { - s.logger.Debug("gsoc ws: set write deadline failed", "error", err) - return - } + case <-wake: + for { + queueMu.Lock() + if len(queue) == 0 { + queueMu.Unlock() + break + } + b := queue[0] + queue = queue[1:] + if len(queue) == 0 { + queue = nil // release the backing array once drained + } + queueMu.Unlock() - err = conn.WriteMessage(websocket.BinaryMessage, b) - if err != nil { - s.logger.Debug("gsoc ws: write message failed", "error", err) - return + err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) + if err != nil { + s.logger.Debug("gsoc ws: set write deadline failed", "error", err) + return + } + + err = conn.WriteMessage(websocket.BinaryMessage, b) + if err != nil { + s.logger.Debug("gsoc ws: write message failed", "error", err) + return + } } case <-s.quit: diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 64b926eef69..4c7c666b466 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -278,15 +278,17 @@ func TestGsocWebsocketInvalidFieldsHeader(t *testing.T) { ) } -// TestGsocWebsocketSlowConsumer verifies that when a subscriber cannot keep up -// with incoming GSOC messages, the server closes the connection instead of -// blocking indefinitely or racing on the underlying websocket connection. +// TestGsocWebsocketSlowConsumer verifies that a subscriber that falls behind +// incoming GSOC messages is not dropped or disconnected: the server queues +// every message (no cap) and delivers the full backlog, in order, once the +// consumer catches up, instead of racing on the underlying websocket +// connection or blocking the (synchronous) GSOC handler indefinitely. // // The connection is served over an in-memory net.Pipe, which is fully // synchronous (unbuffered): a write only completes once a matching read -// consumes it. This makes the small dataC buffer overflow deterministically -// as soon as the client stops reading, instead of depending on the size of -// the OS's (possibly very large, auto-tuned) TCP socket buffers. +// consumes it, so the server-side writer is deterministically blocked on the +// first message for as long as the client doesn't read, while further +// messages still queue up behind it without being dropped. func TestGsocWebsocketSlowConsumer(t *testing.T) { t.Parallel() @@ -338,12 +340,14 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { } testutil.CleanupCloser(t, cl) - // never read from cl, so the dataC buffer (cap 2) fills up almost - // immediately: the first message blocks the single writer goroutine - // (nothing reads the pipe), and the next ones queue up and overflow. + // never read from cl while queuing every message: the first message + // blocks the single writer goroutine (nothing reads the pipe yet), and + // the rest pile up behind it in the unbounded queue instead of being + // dropped. + payloads := make([][]byte, messageCount) for i := range messageCount { - payload := []byte{byte(i)} - ch, _ := cac.New(payload) + payloads[i] = []byte{byte(i)} + ch, _ := cac.New(payloads[i]) socCh := soc.New(id, ch) signedCh, _ := socCh.Sign(signer) socCh, _ = soc.FromChunk(signedCh) @@ -354,16 +358,16 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { t.Fatal(err) } - // Drain whatever messages had already been handed to the (synchronous) - // pipe before the overflow was detected; the connection must eventually - // be closed instead of the server delivering every message regardless of - // how far behind the consumer falls. - var readErr error - for i := 0; i < messageCount && readErr == nil; i++ { - _, _, readErr = cl.ReadMessage() - } - if readErr == nil { - t.Fatal("expected connection to be closed for a slow consumer") + // the whole backlog must arrive, in order, once the consumer starts + // reading again. + for i, want := range payloads { + _, got, err := cl.ReadMessage() + if err != nil { + t.Fatalf("message %d: %v", i, err) + } + if !bytes.Equal(got, want) { + t.Fatalf("message %d: got %q, want %q", i, got, want) + } } } From ccb847eaf6fff72010bac27d78a0b4a39866c2d0 Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 15 Sep 2026 14:13:11 +0200 Subject: [PATCH 17/33] refactor: dedicated queue structure on api --- pkg/api/gsoc.go | 90 +++++++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index a7c03378219..ab90dc36575 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -163,27 +163,16 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { return } - s.wsWg.Add(1) - go s.gsocListeningWs(conn, paths.Address, fields, headers.CacheWrappedChunk) -} - -func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address, fields []string, cacheWrappedChunk bool) { - defer s.wsWg.Done() - - var ( - queueMu sync.Mutex - queue [][]byte - wake = make(chan struct{}, 1) - gone = make(chan struct{}) - ticker = time.NewTicker(s.WsPingPeriod) - err error - ) - defer func() { - ticker.Stop() - _ = conn.Close() - }() - cleanup := s.gsoc.Subscribe(socAddress, func(c *soc.SOC) { - if cacheWrappedChunk { + // Subscribe synchronously, before handing the connection off to its own + // goroutine: Upgrade already flushed the 101 response, so the client can + // start sending GSOC-triggering activity immediately. Subscribing here + // instead of inside the spawned goroutine closes the window in which an + // update could arrive before the handler is registered and be silently + // missed. + queue := &gsocQueue{} + wake := make(chan struct{}, 1) + cleanup := s.gsoc.Subscribe(paths.Address, func(c *soc.SOC) { + if headers.CacheWrappedChunk { // Caching is a node-local side effect independent of this // subscriber's connection, so it must not be aborted just // because the websocket closes mid-write. @@ -198,9 +187,7 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address return } - queueMu.Lock() - queue = append(queue, b) - queueMu.Unlock() + queue.push(b) // Non-blocking: the writer only needs to know there is something to // drain, not one notification per message, so a full wake channel @@ -211,8 +198,52 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address } }) + s.wsWg.Add(1) + go s.gsocListeningWs(conn, cleanup, queue, wake) +} + +// gsocQueue is an unbounded FIFO of pending outgoing GSOC messages: the +// producer (the GSOC subscription callback) must never block or drop a +// message, no matter how far behind the websocket writer falls. +type gsocQueue struct { + mu sync.Mutex + items [][]byte +} + +func (q *gsocQueue) push(b []byte) { + q.mu.Lock() + q.items = append(q.items, b) + q.mu.Unlock() +} + +// pop returns the oldest queued message, or ok=false if the queue is empty. +func (q *gsocQueue) pop() (b []byte, ok bool) { + q.mu.Lock() + defer q.mu.Unlock() + if len(q.items) == 0 { + return nil, false + } + b, q.items = q.items[0], q.items[1:] + if len(q.items) == 0 { + q.items = nil // release the backing array once drained + } + return b, true +} + +func (s *Service) gsocListeningWs(conn *websocket.Conn, cleanup func(), queue *gsocQueue, wake chan struct{}) { + defer s.wsWg.Done() defer cleanup() + var ( + gone = make(chan struct{}) + ticker = time.NewTicker(s.WsPingPeriod) + err error + ) + defer func() { + ticker.Stop() + _ = conn.Close() + }() + conn.SetCloseHandler(func(code int, text string) error { s.logger.Debug("gsoc ws: client gone", "code", code, "message", text) close(gone) @@ -223,17 +254,10 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address select { case <-wake: for { - queueMu.Lock() - if len(queue) == 0 { - queueMu.Unlock() + b, ok := queue.pop() + if !ok { break } - b := queue[0] - queue = queue[1:] - if len(queue) == 0 { - queue = nil // release the backing array once drained - } - queueMu.Unlock() err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) if err != nil { From 525d2b4d94da25fc3483a87230914e42f4de67bc Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 15 Sep 2026 14:55:29 +0200 Subject: [PATCH 18/33] test: improve synchronization in websocket --- pkg/api/gsoc_test.go | 45 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 4c7c666b466..eeb01205137 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -340,6 +340,47 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { } testutil.CleanupCloser(t, cl) + // Subscribe runs synchronously in the HTTP handler before the connection + // is handed off to its own goroutine, but Dial returning only means the + // handshake bytes were exchanged over the pipe — it is not a + // happens-before guarantee that the server has gone on to call + // Subscribe. Those are two independent continuations of the same + // rendezvous, and either goroutine can be scheduled first; a message + // sent right after Dial can race Subscribe and, if it loses, is dropped + // before any handler exists to queue it — permanently, not just + // delayed. Real network I/O (as in TestGsocWebsocketMessageOrdering) + // involves enough actual syscalls to force scheduler yields that this is + // a non-issue in practice, but this test deliberately uses an in-memory + // net.Pipe (see comment above) to avoid depending on real I/O timing, + // which removes that cushion. + // + // Give the server's goroutine time to reach Subscribe before sending + // anything: this can't be done by retrying a read with a short deadline + // instead, because gorilla/websocket permanently poisons a *Conn for all + // further reads once any read — including one that merely times out — + // returns an error, so a read that might legitimately need a retry + // cannot be attempted more than once on the same connection. + time.Sleep(300 * time.Millisecond) + + // Confirm the subscription is actually active (rather than silently + // trusting the sleep) with a single throwaway round-trip. + warmup := []byte{0xff} + { + ch, _ := cac.New(warmup) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + gsocSvc.Handle(socCh) + } + if err := cl.SetReadDeadline(time.Now().Add(longTimeout)); err != nil { + t.Fatal(err) + } + if _, got, err := cl.ReadMessage(); err != nil { + t.Fatalf("warmup message: %v", err) + } else if !bytes.Equal(got, warmup) { + t.Fatalf("warmup message: got %q, want %q", got, warmup) + } + // never read from cl while queuing every message: the first message // blocks the single writer goroutine (nothing reads the pipe yet), and // the rest pile up behind it in the unbounded queue instead of being @@ -354,10 +395,6 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { gsocSvc.Handle(socCh) } - if err := cl.SetReadDeadline(time.Now().Add(longTimeout)); err != nil { - t.Fatal(err) - } - // the whole backlog must arrive, in order, once the consumer starts // reading again. for i, want := range payloads { From 220bb021e57fdae6d020300898cd155c47288c0f Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 10:58:42 +0200 Subject: [PATCH 19/33] refactor: implement bounded queue --- openapi/Swarm.yaml | 3 + pkg/api/export_test.go | 2 + pkg/api/gsoc.go | 72 ++++++++++++++++++---- pkg/api/gsoc_test.go | 134 +++++++++++++++++++++++++++++++---------- 4 files changed, 167 insertions(+), 44 deletions(-) diff --git a/openapi/Swarm.yaml b/openapi/Swarm.yaml index a9d08f7edb4..42e97e094c9 100644 --- a/openapi/Swarm.yaml +++ b/openapi/Swarm.yaml @@ -976,6 +976,9 @@ paths: Single Owner Chunk address. Each message is the binary serialization of the Single Owner Chunk fields requested through the swarm-soc-fields header (defaults to the wrapped chunk payload). + Pending messages are buffered per subscription up to a fixed limit; + a client that does not keep up with the incoming rate misses the + oldest of them, as they are discarded in favor of the newest ones. "400": $ref: "SwarmCommon.yaml#/components/responses/400" "500": diff --git a/pkg/api/export_test.go b/pkg/api/export_test.go index 0eb164a8cbe..25217a91c53 100644 --- a/pkg/api/export_test.go +++ b/pkg/api/export_test.go @@ -45,6 +45,8 @@ var ( SuccessWsMsg = successWsMsg ) +const GsocQueueCapacity = gsocQueueCapacity + var ( FileSizeBucketsKBytes = fileSizeBucketsKBytes ToFileSizeBucket = toFileSizeBucket diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index ab90dc36575..5e8cb2828c1 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -33,6 +33,13 @@ const ( socFieldPayload = "payload" ) +// gsocQueueCapacity is the maximum number of pending outgoing messages held +// per GSOC websocket subscription. Since a serialized message is at most +// maxSocFieldsSize bytes, this caps a single subscription's backlog at a bit +// over 1 MB, while still leaving enough room to absorb the bursts a client +// that keeps reading can be expected to work through. +const gsocQueueCapacity = 256 + var validSocFields = []string{ socFieldAddress, socFieldRecoveredPubKey, @@ -169,7 +176,7 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { // instead of inside the spawned goroutine closes the window in which an // update could arrive before the handler is registered and be silently // missed. - queue := &gsocQueue{} + queue := newGsocQueue() wake := make(chan struct{}, 1) cleanup := s.gsoc.Subscribe(paths.Address, func(c *soc.SOC) { if headers.CacheWrappedChunk { @@ -202,34 +209,69 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { go s.gsocListeningWs(conn, cleanup, queue, wake) } -// gsocQueue is an unbounded FIFO of pending outgoing GSOC messages: the -// producer (the GSOC subscription callback) must never block or drop a -// message, no matter how far behind the websocket writer falls. +// gsocQueue is a bounded FIFO ring buffer of pending outgoing GSOC messages. +// +// The producer (the GSOC subscription callback) runs on the node's chunk +// handling path, so it must never block on the websocket writer; with an +// unbounded queue that would let anyone spamming a subscribed GSOC address +// grow the backlog without limit and exhaust the node's memory whenever the +// client does not keep up. Once the queue is full the oldest pending message +// is therefore evicted to make room for the newest one: for a real-time +// subscription a fresh update is worth more than a stale one. type gsocQueue struct { - mu sync.Mutex - items [][]byte + mu sync.Mutex + items [][]byte // ring buffer, fixed length gsocQueueCapacity + head int // index of the oldest queued message + size int // number of queued messages + dropped uint64 // messages evicted since the last droppedCount call +} + +func newGsocQueue() *gsocQueue { + return &gsocQueue{items: make([][]byte, gsocQueueCapacity)} } +// push queues a message, evicting the oldest one if the queue is full. func (q *gsocQueue) push(b []byte) { q.mu.Lock() - q.items = append(q.items, b) - q.mu.Unlock() + defer q.mu.Unlock() + + if q.size == len(q.items) { + q.items[q.head] = nil + q.head = (q.head + 1) % len(q.items) + q.size-- + q.dropped++ + } + q.items[(q.head+q.size)%len(q.items)] = b + q.size++ } // pop returns the oldest queued message, or ok=false if the queue is empty. func (q *gsocQueue) pop() (b []byte, ok bool) { q.mu.Lock() defer q.mu.Unlock() - if len(q.items) == 0 { + + if q.size == 0 { return nil, false } - b, q.items = q.items[0], q.items[1:] - if len(q.items) == 0 { - q.items = nil // release the backing array once drained - } + b = q.items[q.head] + q.items[q.head] = nil // drop the reference so the message can be collected + q.head = (q.head + 1) % len(q.items) + q.size-- return b, true } +// droppedCount returns how many messages were evicted since the previous call +// and resets the counter, so that a slow subscriber is reported once per drain +// instead of once per lost message. +func (q *gsocQueue) droppedCount() uint64 { + q.mu.Lock() + defer q.mu.Unlock() + + dropped := q.dropped + q.dropped = 0 + return dropped +} + func (s *Service) gsocListeningWs(conn *websocket.Conn, cleanup func(), queue *gsocQueue, wake chan struct{}) { defer s.wsWg.Done() defer cleanup() @@ -272,6 +314,10 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, cleanup func(), queue *g } } + if dropped := queue.droppedCount(); dropped > 0 { + s.logger.Warning("gsoc ws: subscriber too slow, messages dropped", "count", dropped) + } + case <-s.quit: // shutdown err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index eeb01205137..29937794471 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -294,8 +294,105 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { const messageCount = 10 + id := make([]byte, 32) + gsocSvc, cl, signer := newGsocPipeTest(t, id) + + // never read from cl while queuing every message: the first message + // blocks the single writer goroutine (nothing reads the pipe yet), and + // the rest pile up behind it in the queue. messageCount is well below + // api.GsocQueueCapacity, so none of them is evicted. + payloads := make([][]byte, messageCount) + for i := range messageCount { + payloads[i] = []byte{byte(i)} + ch, _ := cac.New(payloads[i]) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + gsocSvc.Handle(socCh) + } + + // the whole backlog must arrive, in order, once the consumer starts + // reading again. + for i, want := range payloads { + _, got, err := cl.ReadMessage() + if err != nil { + t.Fatalf("message %d: %v", i, err) + } + if !bytes.Equal(got, want) { + t.Fatalf("message %d: got %q, want %q", i, got, want) + } + } +} + +// TestGsocWebsocketQueueBound verifies that a subscriber which stops reading +// cannot make the server side queue grow without limit: once the queue is full +// the oldest pending messages are evicted, while the most recent ones are still +// delivered, in order. +func TestGsocWebsocketQueueBound(t *testing.T) { + t.Parallel() + + const messageCount = api.GsocQueueCapacity + 16 + + id := make([]byte, 32) + gsocSvc, cl, signer := newGsocPipeTest(t, id) + + // never read from cl while queuing every message: the first message + // blocks the single writer goroutine (nothing reads the pipe yet), and + // the rest pile up behind it until the queue is full and starts dropping + // its oldest entries. + for i := range messageCount { + payload := []byte{byte(i >> 8), byte(i)} + ch, _ := cac.New(payload) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + gsocSvc.Handle(socCh) + } + + // the newest message is never evicted, so reading until it arrives + // terminates. At most one message can have left the queue before the + // writer blocked on the pipe, which puts the whole backlog the consumer + // can still see at api.GsocQueueCapacity+1 messages. + var ( + received int + last = -1 + ) + for { + _, got, err := cl.ReadMessage() + if err != nil { + t.Fatalf("message %d: %v", received, err) + } + if len(got) != 2 { + t.Fatalf("message %d: got payload %q, want 2 bytes", received, got) + } + received++ + + index := int(got[0])<<8 | int(got[1]) + if index <= last { + t.Fatalf("message %d: got index %d after %d, want increasing order", received, index, last) + } + last = index + + if index == messageCount-1 { + break + } + } + + if received > api.GsocQueueCapacity+1 { + t.Fatalf("got %d messages, want at most %d: the queue is not bounded", received, api.GsocQueueCapacity+1) + } +} + +// newGsocPipeTest subscribes to the GSOC address of socID over an in-memory +// net.Pipe instead of a real socket, so that a test fully controls when the +// client reads: writes to a pipe block until the other side reads, which is +// what makes the server side queue observable. It returns the listener to +// publish through, the client end of the subscription and the signer owning +// the subscribed address. +func newGsocPipeTest(t *testing.T, socID []byte) (gsoc.Listener, *websocket.Conn, crypto.Signer) { + t.Helper() + var ( - id = make([]byte, 32) batchStore = mockbatchstore.New() storer = mockstorer.New() gsocSvc = gsoc.New(log.Noop) @@ -320,7 +417,7 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { if err != nil { t.Fatal(err) } - chunkAddr, _ := soc.CreateAddress(id, owner.Bytes()) + chunkAddr, _ := soc.CreateAddress(socID, owner.Bytes()) ln := newPipeListener() srv := &http.Server{Handler: svc} @@ -350,9 +447,8 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { // before any handler exists to queue it — permanently, not just // delayed. Real network I/O (as in TestGsocWebsocketMessageOrdering) // involves enough actual syscalls to force scheduler yields that this is - // a non-issue in practice, but this test deliberately uses an in-memory - // net.Pipe (see comment above) to avoid depending on real I/O timing, - // which removes that cushion. + // a non-issue in practice, but the in-memory pipe used here removes that + // cushion. // // Give the server's goroutine time to reach Subscribe before sending // anything: this can't be done by retrying a read with a short deadline @@ -367,7 +463,7 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { warmup := []byte{0xff} { ch, _ := cac.New(warmup) - socCh := soc.New(id, ch) + socCh := soc.New(socID, ch) signedCh, _ := socCh.Sign(signer) socCh, _ = soc.FromChunk(signedCh) gsocSvc.Handle(socCh) @@ -381,31 +477,7 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { t.Fatalf("warmup message: got %q, want %q", got, warmup) } - // never read from cl while queuing every message: the first message - // blocks the single writer goroutine (nothing reads the pipe yet), and - // the rest pile up behind it in the unbounded queue instead of being - // dropped. - payloads := make([][]byte, messageCount) - for i := range messageCount { - payloads[i] = []byte{byte(i)} - ch, _ := cac.New(payloads[i]) - socCh := soc.New(id, ch) - signedCh, _ := socCh.Sign(signer) - socCh, _ = soc.FromChunk(signedCh) - gsocSvc.Handle(socCh) - } - - // the whole backlog must arrive, in order, once the consumer starts - // reading again. - for i, want := range payloads { - _, got, err := cl.ReadMessage() - if err != nil { - t.Fatalf("message %d: %v", i, err) - } - if !bytes.Equal(got, want) { - t.Fatalf("message %d: got %q, want %q", i, got, want) - } - } + return gsocSvc, cl, signer } // pipeListener is a net.Listener that hands out pre-established net.Conn From d79f162b0fe423e26fd7277de210fcd12456c69a Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 11:17:00 +0200 Subject: [PATCH 20/33] refactor: replace validSocFields slice --- pkg/api/gsoc.go | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index 5e8cb2828c1..57264028b8d 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -9,7 +9,6 @@ import ( "context" "fmt" "net/http" - "slices" "strings" "sync" "time" @@ -40,45 +39,49 @@ const ( // that keeps reading can be expected to work through. const gsocQueueCapacity = 256 -var validSocFields = []string{ - socFieldAddress, - socFieldRecoveredPubKey, - socFieldIdentifier, - socFieldSignature, - socFieldWrappedAddress, - socFieldSpan, - socFieldPayload, +// socFieldSizes is the single source of truth for the valid SOC fields: it maps +// every field identifier to the maximum number of bytes its serialized form +// occupies. A field added here is accepted by parseSocFields and accounted for +// in maxSocFieldsSize without any further change. +var socFieldSizes = map[string]int{ + socFieldAddress: swarm.HashSize, + socFieldRecoveredPubKey: soc.OwnerPubKeySize, + socFieldIdentifier: swarm.HashSize, + socFieldSignature: swarm.SocSignatureSize, + socFieldWrappedAddress: swarm.HashSize, + socFieldSpan: swarm.SpanSize, + socFieldPayload: swarm.ChunkSize, } // maxSocFieldsSize is the maximum size of a serialized SOC fields message when -// every field is requested: the whole single owner chunk (identifier + -// signature + span + payload, i.e. SocMaxChunkSize) plus the derived metadata -// fields that are not part of the chunk on the wire (soc address, recovered -// public key and wrapped chunk address). -const maxSocFieldsSize = swarm.SocMaxChunkSize + - swarm.HashSize + // soc address - soc.OwnerPubKeySize + // recovered public key - swarm.HashSize // wrapped chunk address +// every field is requested. It is derived from socFieldSizes so that it stays +// correct when fields are added or removed. +var maxSocFieldsSize = func() (size int) { + for _, s := range socFieldSizes { + size += s + } + return size +}() // parseSocFields parses the SwarmSocFieldsHeader value into a list of SOC field // identifiers. When the header is empty it defaults to the payload field only, // which preserves backward compatibility. Duplicate fields are dropped, keeping -// the first occurrence, so the returned slice never exceeds len(validSocFields) +// the first occurrence, so the returned slice never exceeds len(socFieldSizes) // entries regardless of how many times a field is repeated in the header. func parseSocFields(header string) ([]string, error) { if strings.TrimSpace(header) == "" { return []string{socFieldPayload}, nil } - seen := make(map[string]bool, len(validSocFields)) + seen := make(map[string]bool, len(socFieldSizes)) parts := strings.Split(header, ",") - fields := make([]string, 0, len(validSocFields)) + fields := make([]string, 0, len(socFieldSizes)) for _, p := range parts { f := strings.ToLower(strings.TrimSpace(p)) if f == "" { continue } - if !slices.Contains(validSocFields, f) { + if _, ok := socFieldSizes[f]; !ok { return nil, fmt.Errorf("unknown soc field: %q", p) } if seen[f] { From 0105b922030296e87506f50919eeb90c123ff406 Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 11:22:39 +0200 Subject: [PATCH 21/33] refactor: implement GsocQueue with release functionality --- pkg/api/export_test.go | 8 ++++++++ pkg/api/gsoc.go | 36 ++++++++++++++++++++++++++++++------ pkg/api/gsoc_test.go | 21 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/pkg/api/export_test.go b/pkg/api/export_test.go index 25217a91c53..9b869dd2dc8 100644 --- a/pkg/api/export_test.go +++ b/pkg/api/export_test.go @@ -47,6 +47,14 @@ var ( const GsocQueueCapacity = gsocQueueCapacity +type GsocQueue = gsocQueue + +func NewGsocQueue() *GsocQueue { return newGsocQueue() } + +func (q *GsocQueue) Push(b []byte) { q.push(b) } +func (q *GsocQueue) Pop() ([]byte, bool) { return q.pop() } +func (q *GsocQueue) Release() { q.release() } + var ( FileSizeBucketsKBytes = fileSizeBucketsKBytes ToFileSizeBucket = toFileSizeBucket diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index 57264028b8d..a780078c9dd 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -222,22 +222,27 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { // is therefore evicted to make room for the newest one: for a real-time // subscription a fresh update is worth more than a stale one. type gsocQueue struct { - mu sync.Mutex - items [][]byte // ring buffer, fixed length gsocQueueCapacity - head int // index of the oldest queued message - size int // number of queued messages - dropped uint64 // messages evicted since the last droppedCount call + mu sync.Mutex + items [][]byte // ring buffer, fixed length gsocQueueCapacity + head int // index of the oldest queued message + size int // number of queued messages + dropped uint64 // messages evicted since the last droppedCount call + released bool // set once the writer is gone, see release } func newGsocQueue() *gsocQueue { return &gsocQueue{items: make([][]byte, gsocQueueCapacity)} } -// push queues a message, evicting the oldest one if the queue is full. +// push queues a message, evicting the oldest one if the queue is full. It is a +// no-op once the queue has been released. func (q *gsocQueue) push(b []byte) { q.mu.Lock() defer q.mu.Unlock() + if q.released { + return + } if q.size == len(q.items) { q.items[q.head] = nil q.head = (q.head + 1) % len(q.items) @@ -275,8 +280,27 @@ func (q *gsocQueue) droppedCount() uint64 { return dropped } +// release discards the undelivered backlog and stops the queue from accepting +// further messages. Nothing drains the queue once its writer is gone, so the +// pending messages are dead weight from that point on; dropping them here +// frees them right away instead of keeping them alive for as long as a +// producer that is still mid-callback can reach the queue. +func (q *gsocQueue) release() { + q.mu.Lock() + defer q.mu.Unlock() + + clear(q.items) + q.head = 0 + q.size = 0 + q.released = true +} + func (s *Service) gsocListeningWs(conn *websocket.Conn, cleanup func(), queue *gsocQueue, wake chan struct{}) { defer s.wsWg.Done() + // Defers run in reverse order: unsubscribe first, so that no producer can + // queue anything new, and only then drop whatever backlog this connection + // never got to write out. + defer queue.release() defer cleanup() var ( diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 29937794471..ea8949ee249 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -383,6 +383,27 @@ func TestGsocWebsocketQueueBound(t *testing.T) { } } +// TestGsocQueueRelease verifies that the backlog of a subscription whose +// writer is gone is discarded, rather than kept around by a producer that is +// still mid-callback when the connection is torn down. +func TestGsocQueueRelease(t *testing.T) { + t.Parallel() + + q := api.NewGsocQueue() + q.Push([]byte("queued before release")) + + q.Release() + + if b, ok := q.Pop(); ok { + t.Fatalf("got %q after release, want the backlog to be discarded", b) + } + + q.Push([]byte("queued after release")) + if b, ok := q.Pop(); ok { + t.Fatalf("got %q after release, want a late push to be discarded", b) + } +} + // newGsocPipeTest subscribes to the GSOC address of socID over an in-memory // net.Pipe instead of a real socket, so that a test fully controls when the // client reads: writes to a pipe block until the other side reads, which is From 943b885fa9f0844bc719e89da5ff1a85a20e4182 Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 11:29:23 +0200 Subject: [PATCH 22/33] refactor: test server initialization in API tests to include additional parameters - Updated multiple test files to modify the `newTestServer` function calls, adding an extra parameter to accommodate changes in the test server options. - Ensured consistency across various test cases in readiness, redistribution, router, settlements, SOC, staking, status, stewardship, subdomain, tag, topology, tracing, transaction, wallet, and welcome message tests. --- pkg/api/accesscontrol_test.go | 50 ++++++++++----------- pkg/api/accounting_test.go | 4 +- pkg/api/api_test.go | 29 ++++++------ pkg/api/balances_test.go | 24 +++++----- pkg/api/bytes_test.go | 28 ++++++------ pkg/api/bzz_test.go | 46 +++++++++---------- pkg/api/chequebook_test.go | 32 +++++++------- pkg/api/chunk_stream_test.go | 12 ++--- pkg/api/chunk_test.go | 30 ++++++------- pkg/api/cors_test.go | 6 +-- pkg/api/debugstorage_test.go | 2 +- pkg/api/dirs_test.go | 8 ++-- pkg/api/envelope_test.go | 6 +-- pkg/api/feed_test.go | 36 +++++++-------- pkg/api/gsoc_test.go | 8 ++-- pkg/api/health_test.go | 4 +- pkg/api/logger_test.go | 8 ++-- pkg/api/p2p_test.go | 4 +- pkg/api/peer_test.go | 16 +++---- pkg/api/pin_test.go | 10 ++--- pkg/api/pingpong_test.go | 4 +- pkg/api/postage_test.go | 78 ++++++++++++++++----------------- pkg/api/pss_test.go | 10 ++--- pkg/api/readiness_test.go | 4 +- pkg/api/redistribution_test.go | 22 +++++----- pkg/api/router_test.go | 2 +- pkg/api/settlements_test.go | 14 +++--- pkg/api/soc_test.go | 24 +++++----- pkg/api/staking_test.go | 40 ++++++++--------- pkg/api/status_test.go | 4 +- pkg/api/stewardship_test.go | 12 ++--- pkg/api/subdomain_test.go | 8 ++-- pkg/api/tag_test.go | 8 ++-- pkg/api/topology_test.go | 2 +- pkg/api/tracing_test.go | 2 +- pkg/api/transaction_test.go | 20 ++++----- pkg/api/wallet_test.go | 32 +++++++------- pkg/api/welcome_message_test.go | 6 +-- 38 files changed, 325 insertions(+), 330 deletions(-) diff --git a/pkg/api/accesscontrol_test.go b/pkg/api/accesscontrol_test.go index c6c889f58c2..946c732dceb 100644 --- a/pkg/api/accesscontrol_test.go +++ b/pkg/api/accesscontrol_test.go @@ -153,7 +153,7 @@ func TestAccessLogicEachEndpointWithAct(t *testing.T) { upTestOpts = append(upTestOpts, jsonhttptest.WithRequestHeader(api.SwarmCollectionHeader, "True")) } t.Run(v.name, func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -213,7 +213,7 @@ func TestAccessLogicWithoutAct(t *testing.T) { ) t.Run("upload-w/-act-then-download-w/o-act", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -246,7 +246,7 @@ func TestAccessLogicWithoutAct(t *testing.T) { }) t.Run("upload-w/o-act-then-download-w/-act", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -310,7 +310,7 @@ func TestAccessLogicInvalidPath(t *testing.T) { ) t.Run("invalid-path-params", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -361,7 +361,7 @@ func TestAccessLogicHistory(t *testing.T) { ) t.Run("empty-history-upload-then-download-and-check-data", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -397,7 +397,7 @@ func TestAccessLogicHistory(t *testing.T) { }) t.Run("with-history-upload-then-download-and-check-data", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -443,7 +443,7 @@ func TestAccessLogicHistory(t *testing.T) { }) t.Run("upload-then-download-wrong-history", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -479,7 +479,7 @@ func TestAccessLogicHistory(t *testing.T) { }) t.Run("upload-wrong-history", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -502,7 +502,7 @@ func TestAccessLogicHistory(t *testing.T) { }) t.Run("download-w/o-history", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -538,7 +538,7 @@ func TestAccessLogicTimestamp(t *testing.T) { fileName = "sample.html" ) t.Run("upload-then-download-with-timestamp-and-check-data", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -586,7 +586,7 @@ func TestAccessLogicTimestamp(t *testing.T) { t.Run("download-w/o-timestamp", func(t *testing.T) { encryptedRef := "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -601,7 +601,7 @@ func TestAccessLogicTimestamp(t *testing.T) { ) }) t.Run("download-w/-invalid-timestamp", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -649,7 +649,7 @@ func TestAccessLogicPublisher(t *testing.T) { ) t.Run("upload-then-download-w/-publisher-and-check-data", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -695,7 +695,7 @@ func TestAccessLogicPublisher(t *testing.T) { }) t.Run("upload-then-download-invalid-publickey", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -753,7 +753,7 @@ func TestAccessLogicPublisher(t *testing.T) { downloader = "03c712a7e29bc792ac8d8ae49793d28d5bda27ed70f0d90697b2fb456c0a168bd2" encryptedRef = "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" ) - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -778,7 +778,7 @@ func TestAccessLogicPublisher(t *testing.T) { downloader = "03c712a7e29bc792ac8d8ae49793d28d5bda27ed70f0d90697b2fb456c0a168bd2" testfile = "testfile1" ) - downloaderClient, _, _, _ := newTestServer(t, testServerOptions{ + downloaderClient, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -800,7 +800,7 @@ func TestAccessLogicPublisher(t *testing.T) { t.Run("download-w/o-publisher", func(t *testing.T) { encryptedRef := "a5df670544eaea29e61b19d8739faa4573b19e4426e58a173e51ed0b5e7e2ade" - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -820,13 +820,13 @@ func TestAccessLogicPublisher(t *testing.T) { func TestAccessLogicGrantees(t *testing.T) { t.Parallel() var ( - spk, _ = hex.DecodeString("a786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") - pk, _ = crypto.DecodeSecp256k1PrivateKey(spk) - storerMock = mockstorer.New() - h, fixtureHref = prepareHistoryFixture(storerMock) - logger = log.Noop - addr = swarm.RandAddress(t) - client, _, _, _ = newTestServer(t, testServerOptions{ + spk, _ = hex.DecodeString("a786dd84b61485de12146fd9c4c02d87e8fd95f0542765cb7fc3d2e428c0bcfa") + pk, _ = crypto.DecodeSecp256k1PrivateKey(spk) + storerMock = mockstorer.New() + h, fixtureHref = prepareHistoryFixture(storerMock) + logger = log.Noop + addr = swarm.RandAddress(t) + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -839,7 +839,7 @@ func TestAccessLogicGrantees(t *testing.T) { publicKeyBytes = crypto.EncodeSecp256k1PublicKey(&pk.PublicKey) publisher = hex.EncodeToString(publicKeyBytes) ) - clientwihtpublisher, _, _, _ := newTestServer(t, testServerOptions{ + clientwihtpublisher, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), diff --git a/pkg/api/accounting_test.go b/pkg/api/accounting_test.go index 901ca8f2883..5067bfb516b 100644 --- a/pkg/api/accounting_test.go +++ b/pkg/api/accounting_test.go @@ -55,7 +55,7 @@ func TestAccountingInfo(t *testing.T) { return ret, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithPeerAccountingFunc(accountingFunc)}, }) @@ -109,7 +109,7 @@ func TestAccountingInfoError(t *testing.T) { accountingFunc := func() (map[string]accounting.PeerInfo, error) { return nil, wantErr } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithPeerAccountingFunc(accountingFunc)}, }) diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 58edd046f80..b31459cc2ad 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -139,13 +139,14 @@ type testServerOptions struct { ChequebookDisabled bool SwapDisabled bool Erc20ServiceNil bool - // ServiceOut, when set, receives the constructed *api.Service so tests - // can drive it directly (e.g. via a custom net.Listener) instead of - // through the httptest.Server this function also sets up. - ServiceOut **api.Service } -func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket.Conn, string, *chanStorer) { +// newTestServer returns an http client and, when o.WsPath is set, a websocket +// connection, both wired to an httptest.Server serving the api service, the +// address that server listens on, the chan storer set up by o.DirectUpload and +// the api service itself, for tests that need to drive it directly (e.g. over +// a custom net.Listener) instead of through the httptest.Server. +func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket.Conn, string, *chanStorer, *api.Service) { t.Helper() pk, _ := crypto.GenerateSecp256k1Key() signer := crypto.NewDefaultSigner(pk) @@ -259,10 +260,6 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. s.EnableFullAPI() } - if o.ServiceOut != nil { - *o.ServiceOut = s - } - if o.DirectUpload { chanStore = newChanStore(o.Storer.PusherFeed()) t.Cleanup(chanStore.stop) @@ -312,7 +309,7 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. } } - return httpClient, conn, ts.Listener.Addr().String(), chanStore + return httpClient, conn, ts.Listener.Addr().String(), chanStore, s } func pipelineFactory(s storage.Putter, encrypt bool, rLevel redundancy.Level) func() pipeline.Interface { @@ -462,7 +459,7 @@ func TestPostageHeaderError(t *testing.T) { t.Run(endpoint+": empty batch", func(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, @@ -477,7 +474,7 @@ func TestPostageHeaderError(t *testing.T) { }) t.Run(endpoint+": ok batch", func(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, @@ -493,7 +490,7 @@ func TestPostageHeaderError(t *testing.T) { }) t.Run(endpoint+": bad batch", func(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, @@ -513,7 +510,7 @@ func TestPostageHeaderError(t *testing.T) { func TestOptions(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) for _, tc := range []struct { endpoint string expectedMethods string // expectedMethods contains HTTP methods like GET, POST, HEAD, PATCH, DELETE, OPTIONS. These are in alphabetical sorted order @@ -564,7 +561,7 @@ func TestPostageDirectAndDeferred(t *testing.T) { t.Parallel() mockStorer := mockstorer.New() - client, _, _, chanStorer := newTestServer(t, testServerOptions{ + client, _, _, chanStorer, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, @@ -600,7 +597,7 @@ func TestPostageDirectAndDeferred(t *testing.T) { t.Parallel() mockStorer := mockstorer.New() - client, _, _, chanStorer := newTestServer(t, testServerOptions{ + client, _, _, chanStorer, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, diff --git a/pkg/api/balances_test.go b/pkg/api/balances_test.go index fc6f6a25b3a..27ffaae1ecf 100644 --- a/pkg/api/balances_test.go +++ b/pkg/api/balances_test.go @@ -31,7 +31,7 @@ func TestBalances(t *testing.T) { return ret, err } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithCompensatedBalancesFunc(compensatedBalancesFunc)}, }) @@ -70,7 +70,7 @@ func TestBalancesError(t *testing.T) { compensatedBalancesFunc := func() (ret map[string]*big.Int, err error) { return nil, wantErr } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithCompensatedBalancesFunc(compensatedBalancesFunc)}, }) @@ -89,7 +89,7 @@ func TestBalancesPeers(t *testing.T) { compensatedBalanceFunc := func(swarm.Address) (*big.Int, error) { return big.NewInt(100000000000000000), nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithCompensatedBalanceFunc(compensatedBalanceFunc)}, }) @@ -109,7 +109,7 @@ func TestBalancesPeersError(t *testing.T) { compensatedBalanceFunc := func(swarm.Address) (*big.Int, error) { return nil, wantErr } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithCompensatedBalanceFunc(compensatedBalanceFunc)}, }) @@ -128,7 +128,7 @@ func TestBalancesPeersNoBalance(t *testing.T) { compensatedBalanceFunc := func(swarm.Address) (*big.Int, error) { return nil, accounting.ErrPeerNoBalance } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithCompensatedBalanceFunc(compensatedBalanceFunc)}, }) @@ -180,7 +180,7 @@ func TestConsumedBalances(t *testing.T) { ret["PARTY"] = big.NewInt(0) return ret, err } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithBalancesFunc(balancesFunc)}, }) @@ -219,7 +219,7 @@ func TestConsumedError(t *testing.T) { balancesFunc := func() (ret map[string]*big.Int, err error) { return nil, wantErr } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithBalancesFunc(balancesFunc)}, }) @@ -238,7 +238,7 @@ func TestConsumedPeers(t *testing.T) { balanceFunc := func(swarm.Address) (*big.Int, error) { return big.NewInt(1000000000000000000), nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithBalanceFunc(balanceFunc)}, }) @@ -258,7 +258,7 @@ func TestConsumedPeersError(t *testing.T) { balanceFunc := func(swarm.Address) (*big.Int, error) { return nil, wantErr } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithBalanceFunc(balanceFunc)}, }) @@ -277,7 +277,7 @@ func TestConsumedPeersNoBalance(t *testing.T) { balanceFunc := func(swarm.Address) (*big.Int, error) { return nil, accounting.ErrPeerNoBalance } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ AccountingOpts: []mock.Option{mock.WithBalanceFunc(balanceFunc)}, }) @@ -292,7 +292,7 @@ func TestConsumedPeersNoBalance(t *testing.T) { func Test_peerBalanceHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -340,7 +340,7 @@ func Test_peerBalanceHandler_invalidInputs(t *testing.T) { func Test_compensatedPeerBalanceHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string diff --git a/pkg/api/bytes_test.go b/pkg/api/bytes_test.go index 576e263983d..b2f59547395 100644 --- a/pkg/api/bytes_test.go +++ b/pkg/api/bytes_test.go @@ -37,9 +37,9 @@ func TestBytes(t *testing.T) { ) var ( - storerMock = mockstorer.New() - logger = log.Noop - client, _, _, _ = newTestServer(t, testServerOptions{ + storerMock = mockstorer.New() + logger = log.Noop + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -174,7 +174,7 @@ func TestBytesInvalidStamp(t *testing.T) { } t.Run("upload batch not found", func(t *testing.T) { - clientBatchNotExists, _, _, _ := newTestServer(t, testServerOptions{ + clientBatchNotExists, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(), @@ -209,7 +209,7 @@ func TestBytesInvalidStamp(t *testing.T) { retErr = errors.New("err happened") t.Run("upload batch exists error", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -233,7 +233,7 @@ func TestBytesInvalidStamp(t *testing.T) { }) t.Run("upload batch unusable", func(t *testing.T) { - clientBatchUnusable, _, _, _ := newTestServer(t, testServerOptions{ + clientBatchUnusable, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -248,7 +248,7 @@ func TestBytesInvalidStamp(t *testing.T) { }) t.Run("upload invalid tag", func(t *testing.T) { - clientInvalidTag, _, _, _ := newTestServer(t, testServerOptions{ + clientInvalidTag, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -263,7 +263,7 @@ func TestBytesInvalidStamp(t *testing.T) { }) t.Run("upload tag not found", func(t *testing.T) { - clientTagExists, _, _, _ := newTestServer(t, testServerOptions{ + clientTagExists, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -281,7 +281,7 @@ func TestBytesInvalidStamp(t *testing.T) { func TestBytesUploadHandlerInvalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.New(), }) @@ -332,7 +332,7 @@ func TestBytesUploadHandlerInvalidInputs(t *testing.T) { func TestBytesGetHandlerInvalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -380,7 +380,7 @@ func TestBytesGetHandlerInvalidInputs(t *testing.T) { func TestBytesRedundancyLevel(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.New(), Post: mockpost.New(mockpost.WithAcceptAll()), }) @@ -458,7 +458,7 @@ func TestBytesHead(t *testing.T) { t.Run(fmt.Sprintf("level %d encrypt %v", level, encrypt), func(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.New(), Post: mockpost.New(mockpost.WithAcceptAll()), }) @@ -513,7 +513,7 @@ func TestBytesHeadRangeAndConditional(t *testing.T) { t.Fatal(err) } - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.New(), Post: mockpost.New(mockpost.WithAcceptAll()), }) @@ -599,7 +599,7 @@ func TestBytesHeadRangeAndConditional(t *testing.T) { func TestBytesHeadErrorsMatchGet(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.New(), Post: mockpost.New(mockpost.WithAcceptAll()), }) diff --git a/pkg/api/bzz_test.go b/pkg/api/bzz_test.go index c3c3cfcd75a..3068447d925 100644 --- a/pkg/api/bzz_test.go +++ b/pkg/api/bzz_test.go @@ -77,7 +77,7 @@ func TestBzzUploadDownloadWithRedundancy(t *testing.T) { } store := mockstorer.NewForgettingStore(inmemchunkstore.New()) storerMock := mockstorer.NewWithChunkStore(store) - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: log.Noop, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -248,7 +248,7 @@ func TestBzzFiles(t *testing.T) { simpleData = []byte("this is a simple text") storerMock = mockstorer.New() logger = log.Noop - client, _, _, _ = newTestServer(t, testServerOptions{ + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -694,7 +694,7 @@ func TestBzzFilesRangeRequests(t *testing.T) { t.Parallel() logger := log.Noop - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.New(), Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -830,11 +830,11 @@ func TestFeedIndirection(t *testing.T) { // first, "upload" some content for the update var ( - updateData = []byte("

Swarm Feeds Hello World!

") - logger = log.Noop - storer = mockstorer.New() - ctx = context.Background() - client, _, _, _ = newTestServer(t, testServerOptions{ + updateData = []byte("

Swarm Feeds Hello World!

") + logger = log.Noop + storer = mockstorer.New() + ctx = context.Background() + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storer, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -910,7 +910,7 @@ func TestFeedIndirection(t *testing.T) { look = newMockLookup(-1, 0, feedUpdate, nil, &id{}, nil) factory = newMockFactory(look) ) - client, _, _, _ = newTestServer(t, testServerOptions{ + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storer, Logger: logger, Feeds: factory, @@ -939,7 +939,7 @@ func TestFeedIndirection(t *testing.T) { look = newMockLookup(-1, 0, socRootCh, nil, &id{}, nil) factory = newMockFactory(look) ) - client, _, _, _ = newTestServer(t, testServerOptions{ + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storer, Logger: logger, Feeds: factory, @@ -960,7 +960,7 @@ func TestFeedIndirection(t *testing.T) { func Test_bzzDownloadHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -1030,7 +1030,7 @@ func TestInvalidBzzParams(t *testing.T) { }, }, }) - clientBatchUnusable, _, _, _ := newTestServer(t, testServerOptions{ + clientBatchUnusable, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -1057,7 +1057,7 @@ func TestInvalidBzzParams(t *testing.T) { }, }, }) - clientBatchExists, _, _, _ := newTestServer(t, testServerOptions{ + clientBatchExists, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -1084,7 +1084,7 @@ func TestInvalidBzzParams(t *testing.T) { }, }, }) - clientBatchExists, _, _, _ := newTestServer(t, testServerOptions{ + clientBatchExists, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(), @@ -1110,7 +1110,7 @@ func TestInvalidBzzParams(t *testing.T) { }, }, }) - clientInvalidTag, _, _, _ := newTestServer(t, testServerOptions{ + clientInvalidTag, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -1137,7 +1137,7 @@ func TestInvalidBzzParams(t *testing.T) { }, }, }) - clientTagExists, _, _, _ := newTestServer(t, testServerOptions{ + clientTagExists, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -1154,7 +1154,7 @@ func TestInvalidBzzParams(t *testing.T) { t.Run("address not found", func(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -1168,10 +1168,10 @@ func TestInvalidBzzParams(t *testing.T) { func TestBzzDownloadHeaders(t *testing.T) { t.Parallel() var ( - data = []byte("

Swarm Hello World!

") - logger = log.Noop - storer = mockstorer.New() - testServer, _, _, _ = newTestServer(t, testServerOptions{ + data = []byte("

Swarm Hello World!

") + logger = log.Noop + storer = mockstorer.New() + testServer, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storer, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -1218,7 +1218,7 @@ func TestBzzDownloadHeaders(t *testing.T) { func TestBzzUploadRedundancyLevel(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.New(), Post: mockpost.New(mockpost.WithAcceptAll()), }) @@ -1284,7 +1284,7 @@ func TestBzzUploadRedundancyLevel(t *testing.T) { func TestBzzDownloadRedundancyLevel(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.New(), Post: mockpost.New(mockpost.WithAcceptAll()), }) diff --git a/pkg/api/chequebook_test.go b/pkg/api/chequebook_test.go index 86e918a1bad..3a64a36399e 100644 --- a/pkg/api/chequebook_test.go +++ b/pkg/api/chequebook_test.go @@ -39,7 +39,7 @@ func TestChequebookBalance(t *testing.T) { return returnedAvailableBalance, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ ChequebookOpts: []mock.Option{ mock.WithChequebookBalanceFunc(chequebookBalanceFunc), mock.WithChequebookAvailableBalanceFunc(chequebookAvailableBalanceFunc), @@ -69,7 +69,7 @@ func TestChequebookBalanceError(t *testing.T) { return big.NewInt(0), wantErr } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ ChequebookOpts: []mock.Option{mock.WithChequebookBalanceFunc(chequebookBalanceFunc)}, }) @@ -92,7 +92,7 @@ func TestChequebookAvailableBalanceError(t *testing.T) { return nil, errors.New("New errors") } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ ChequebookOpts: []mock.Option{ mock.WithChequebookBalanceFunc(chequebookBalanceFunc), mock.WithChequebookAvailableBalanceFunc(chequebookAvailableBalanceFunc), @@ -114,7 +114,7 @@ func TestChequebookAddress(t *testing.T) { return common.HexToAddress("0xfffff") } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ ChequebookOpts: []mock.Option{mock.WithChequebookAddressFunc(chequebookAddressFunc)}, }) @@ -149,7 +149,7 @@ func TestChequebookWithdraw(t *testing.T) { return common.Hash{}, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ ChequebookOpts: []mock.Option{mock.WithChequebookWithdrawFunc(chequebookWithdrawFunc)}, }) @@ -178,7 +178,7 @@ func TestChequebookWithdraw(t *testing.T) { return common.Hash{}, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ ChequebookOpts: []mock.Option{mock.WithChequebookWithdrawFunc(chequebookWithdrawFunc)}, }) @@ -211,7 +211,7 @@ func TestChequebookDeposit(t *testing.T) { return common.Hash{}, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ ChequebookOpts: []mock.Option{mock.WithChequebookDepositFunc(chequebookDepositFunc)}, }) @@ -241,7 +241,7 @@ func TestChequebookDeposit(t *testing.T) { return common.Hash{}, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ ChequebookOpts: []mock.Option{mock.WithChequebookDepositFunc(chequebookDepositFunc)}, }) @@ -349,7 +349,7 @@ func TestChequebookLastCheques(t *testing.T) { return lastReceivedCheques, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []swapmock.Option{swapmock.WithLastReceivedChequesFunc(lastReceivedChequesFunc), swapmock.WithLastSentChequesFunc(lastSentChequesFunc)}, }) @@ -459,7 +459,7 @@ func TestChequebookLastChequesPeer(t *testing.T) { return lastReceivedCheque, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []swapmock.Option{swapmock.WithLastReceivedChequeFunc(lastReceivedChequeFunc), swapmock.WithLastSentChequeFunc(lastSentChequeFunc)}, }) @@ -497,7 +497,7 @@ func TestChequebookCashout(t *testing.T) { return deployCashingHash, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []swapmock.Option{swapmock.WithCashChequeFunc(cashChequeFunc)}, }) @@ -527,7 +527,7 @@ func TestChequebookCashout_CustomGas(t *testing.T) { return deployCashingHash, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []swapmock.Option{swapmock.WithCashChequeFunc(cashChequeFunc)}, }) @@ -602,7 +602,7 @@ func TestChequebookCashoutStatus(t *testing.T) { return status, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []swapmock.Option{swapmock.WithCashoutStatusFunc(cashoutStatusFunc)}, }) @@ -649,7 +649,7 @@ func TestChequebookCashoutStatus(t *testing.T) { return status, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []swapmock.Option{swapmock.WithCashoutStatusFunc(cashoutStatusFunc)}, }) @@ -687,7 +687,7 @@ func TestChequebookCashoutStatus(t *testing.T) { return status, nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []swapmock.Option{swapmock.WithCashoutStatusFunc(cashoutStatusFunc)}, }) @@ -714,7 +714,7 @@ func TestChequebookCashoutStatus(t *testing.T) { func Test_chequebookLastPeerHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string diff --git a/pkg/api/chunk_stream_test.go b/pkg/api/chunk_stream_test.go index add7165fa89..0ae950134b9 100644 --- a/pkg/api/chunk_stream_test.go +++ b/pkg/api/chunk_stream_test.go @@ -30,8 +30,8 @@ func TestChunkUploadStream(t *testing.T) { wsHeaders.Set(api.SwarmPostageBatchIdHeader, batchOkStr) var ( - storerMock = mockstorer.New() - _, wsConn, _, chanStorer = newTestServer(t, testServerOptions{ + storerMock = mockstorer.New() + _, wsConn, _, chanStorer, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Post: mockpost.New(mockpost.WithAcceptAll()), WsPath: "/chunks/stream", @@ -149,8 +149,8 @@ func TestChunkUploadStreamWithStamp(t *testing.T) { wsHeaders.Set(api.ContentTypeHeader, "application/octet-stream") var ( - storerMock = mockstorer.New() - _, wsConn, _, chanStorer = newTestServer(t, testServerOptions{ + storerMock = mockstorer.New() + _, wsConn, _, chanStorer, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Post: mockpost.New(mockpost.WithAcceptAll()), BatchStore: batchStore, @@ -206,8 +206,8 @@ func TestChunkUploadStreamInvalidStamp(t *testing.T) { wsHeaders.Set(api.ContentTypeHeader, "application/octet-stream") var ( - storerMock = mockstorer.New() - _, wsConn, _, _ = newTestServer(t, testServerOptions{ + storerMock = mockstorer.New() + _, wsConn, _, _, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Post: mockpost.New(mockpost.WithAcceptAll()), WsPath: "/chunks/stream", diff --git a/pkg/api/chunk_test.go b/pkg/api/chunk_test.go index b2512aa2ae8..f730119fcc3 100644 --- a/pkg/api/chunk_test.go +++ b/pkg/api/chunk_test.go @@ -37,11 +37,11 @@ func TestChunkUploadDownload(t *testing.T) { t.Parallel() var ( - chunksEndpoint = "/chunks" - chunksResource = func(a swarm.Address) string { return "/chunks/" + a.String() } - chunk = testingc.GenerateTestRandomChunk() - storerMock = mockstorer.New() - client, _, _, chanStorer = newTestServer(t, testServerOptions{ + chunksEndpoint = "/chunks" + chunksResource = func(a swarm.Address) string { return "/chunks/" + a.String() } + chunk = testingc.GenerateTestRandomChunk() + storerMock = mockstorer.New() + client, _, _, chanStorer, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Post: mockpost.New(mockpost.WithAcceptAll()), DirectUpload: true, @@ -104,7 +104,7 @@ func TestChunkUploadDownload(t *testing.T) { // nolint:paralleltest,tparallel func TestChunkHasHandler(t *testing.T) { mockStorer := mockstorer.New() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, }) @@ -141,7 +141,7 @@ func TestChunkHasHandler(t *testing.T) { func TestChunkHandlersInvalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -203,7 +203,7 @@ func TestChunkInvalidParams(t *testing.T) { t.Run("batch unusable", func(t *testing.T) { t.Parallel() - clientBatchUnusable, _, _, _ := newTestServer(t, testServerOptions{ + clientBatchUnusable, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -219,7 +219,7 @@ func TestChunkInvalidParams(t *testing.T) { t.Run("batch exists", func(t *testing.T) { t.Parallel() - clientBatchExists, _, _, _ := newTestServer(t, testServerOptions{ + clientBatchExists, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -235,7 +235,7 @@ func TestChunkInvalidParams(t *testing.T) { t.Run("batch not found", func(t *testing.T) { t.Parallel() - clientBatchNotFound, _, _, _ := newTestServer(t, testServerOptions{ + clientBatchNotFound, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Logger: logger, Post: mockpost.New(), @@ -253,11 +253,11 @@ func TestPreSignedUpload(t *testing.T) { t.Parallel() var ( - chunksEndpoint = "/chunks" - chunk = testingc.GenerateTestRandomChunk() - storerMock = mockstorer.New() - batchStore = mockbatchstore.New() - client, _, _, _ = newTestServer(t, testServerOptions{ + chunksEndpoint = "/chunks" + chunk = testingc.GenerateTestRandomChunk() + storerMock = mockstorer.New() + batchStore = mockbatchstore.New() + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storerMock, BatchStore: batchStore, }) diff --git a/pkg/api/cors_test.go b/pkg/api/cors_test.go index 715987edf42..cbeb8a9afdd 100644 --- a/pkg/api/cors_test.go +++ b/pkg/api/cors_test.go @@ -85,7 +85,7 @@ func TestCORSHeaders(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ CORSAllowedOrigins: tc.allowedOrigins, }) @@ -162,7 +162,7 @@ func TestCors(t *testing.T) { t.Run(tc.endpoint, func(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ CORSAllowedOrigins: []string{origin}, }) @@ -222,7 +222,7 @@ func TestCorsStatus(t *testing.T) { t.Run(tc.endpoint, func(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ CORSAllowedOrigins: []string{origin}, }) diff --git a/pkg/api/debugstorage_test.go b/pkg/api/debugstorage_test.go index dc13f8d81bf..92fb8aa1aac 100644 --- a/pkg/api/debugstorage_test.go +++ b/pkg/api/debugstorage_test.go @@ -34,7 +34,7 @@ func TestDebugStorage(t *testing.T) { }, } - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.NewWithDebugInfo(want), }) diff --git a/pkg/api/dirs_test.go b/pkg/api/dirs_test.go index 3f7ac099ba5..4c301173120 100644 --- a/pkg/api/dirs_test.go +++ b/pkg/api/dirs_test.go @@ -35,7 +35,7 @@ func TestDirs(t *testing.T) { bzzDownloadResource = func(addr, path string) string { return "/bzz/" + addr + "/" + path } ctx = context.Background() storer = mockstorer.New() - client, _, _, _ = newTestServer(t, testServerOptions{ + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storer, PreventRedirect: true, Post: mockpost.New(mockpost.WithAcceptAll()), @@ -501,9 +501,9 @@ func TestDirsEmtpyDir(t *testing.T) { t.Parallel() var ( - dirUploadResource = "/bzz" - storer = mockstorer.New() - client, _, _, _ = newTestServer(t, testServerOptions{ + dirUploadResource = "/bzz" + storer = mockstorer.New() + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storer, PreventRedirect: true, Post: mockpost.New(mockpost.WithAcceptAll()), diff --git a/pkg/api/envelope_test.go b/pkg/api/envelope_test.go index ede77e63875..3e523b39420 100644 --- a/pkg/api/envelope_test.go +++ b/pkg/api/envelope_test.go @@ -21,7 +21,7 @@ func TestPostEnvelope(t *testing.T) { zeroHex := "0000000000000000000000000000000000000000000000000000000000000000" envelopeEndpoint := func(chunkAddress string) string { return fmt.Sprintf("/envelope/%s", chunkAddress) } - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Post: mockpost.New(mockpost.WithAcceptAll()), }) @@ -43,7 +43,7 @@ func TestPostEnvelope(t *testing.T) { t.Run("postage does not exist", func(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) jsonhttptest.Request(t, client, http.MethodPost, envelopeEndpoint(zeroHex), http.StatusNotFound, jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, zeroHex), @@ -53,7 +53,7 @@ func TestPostEnvelope(t *testing.T) { t.Run("batch unusable", func(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Post: mockpost.New(mockpost.WithAcceptAll()), BatchStore: mockbatchstore.New(), }) diff --git a/pkg/api/feed_test.go b/pkg/api/feed_test.go index 4429d1dc7c2..a5645349881 100644 --- a/pkg/api/feed_test.go +++ b/pkg/api/feed_test.go @@ -71,12 +71,12 @@ func TestFeed_Get(t *testing.T) { t.Parallel() var ( - timestamp = int64(12121212) - ch = toChunk(t, uint64(timestamp), mockWrappedCh.Address().Bytes()) - look = newMockLookup(12, 0, ch, nil, &id{}, &id{}) - factory = newMockFactory(look) - idBytes, _ = (&id{}).MarshalBinary() - client, _, _, _ = newTestServer(t, testServerOptions{ + timestamp = int64(12121212) + ch = toChunk(t, uint64(timestamp), mockWrappedCh.Address().Bytes()) + look = newMockLookup(12, 0, ch, nil, &id{}, &id{}) + factory = newMockFactory(look) + idBytes, _ = (&id{}).MarshalBinary() + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: mockStorer, Feeds: factory, }) @@ -104,7 +104,7 @@ func TestFeed_Get(t *testing.T) { factory = newMockFactory(look) idBytes, _ = (&id{}).MarshalBinary() - client, _, _, _ = newTestServer(t, testServerOptions{ + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: mockStorer, Feeds: factory, }) @@ -134,7 +134,7 @@ func TestFeed_Get(t *testing.T) { factory = newMockFactory(look) idBytes, _ = (&id{}).MarshalBinary() - client, _, _, _ = newTestServer(t, testServerOptions{ + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: mockStorer, Feeds: factory, }) @@ -171,7 +171,7 @@ func TestFeed_Get(t *testing.T) { look = newMockLookup(-1, 2, ch, nil, &id{}, &id{}) factory = newMockFactory(look) - client, _, _, _ = newTestServer(t, testServerOptions{ + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: mockStorer, Feeds: factory, }) @@ -184,9 +184,9 @@ func TestFeed_Get(t *testing.T) { t.Parallel() var ( - look = newMockLookup(1, 0, nil, errors.New("dummy"), &id{}, &id{}) - factory = newMockFactory(look) - client, _, _, _ = newTestServer(t, testServerOptions{ + look = newMockLookup(1, 0, nil, errors.New("dummy"), &id{}, &id{}) + factory = newMockFactory(look) + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: mockStorer, Feeds: factory, }) @@ -218,7 +218,7 @@ func TestFeed_Get(t *testing.T) { factory = newMockFactory(look) idBytes, _ = (&id{}).MarshalBinary() - client, _, _, _ = newTestServer(t, testServerOptions{ + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: mockStorer, Feeds: factory, }) @@ -260,11 +260,11 @@ func TestFeed_Post(t *testing.T) { // get the reference from the store, unmarshal to a // manifest entry and make sure all metadata correct var ( - logger = log.Noop - topic = "aabbcc" - mp = mockpost.New(mockpost.WithIssuer(postage.NewStampIssuer("", "", batchOk, big.NewInt(3), 11, 10, 1000, true))) - mockStorer = mockstorer.New() - client, _, _, _ = newTestServer(t, testServerOptions{ + logger = log.Noop + topic = "aabbcc" + mp = mockpost.New(mockpost.WithIssuer(postage.NewStampIssuer("", "", batchOk, big.NewInt(3), 11, 10, 1000, true))) + mockStorer = mockstorer.New() + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: mockStorer, Logger: logger, Post: mp, diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index ea8949ee249..e1d74099b5c 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -262,7 +262,7 @@ func TestGsocWebsocketInvalidFieldsHeader(t *testing.T) { ) testutil.CleanupCloser(t, gsocSvc) - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Gsoc: gsocSvc, Storer: storer, BatchStore: batchStore, @@ -417,16 +417,14 @@ func newGsocPipeTest(t *testing.T, socID []byte) (gsoc.Listener, *websocket.Conn batchStore = mockbatchstore.New() storer = mockstorer.New() gsocSvc = gsoc.New(log.Noop) - svc *api.Service ) testutil.CleanupCloser(t, gsocSvc) - newTestServer(t, testServerOptions{ + _, _, _, _, svc := newTestServer(t, testServerOptions{ Gsoc: gsocSvc, Storer: storer, BatchStore: batchStore, Logger: log.Noop, - ServiceOut: &svc, }) privKey, err := crypto.GenerateSecp256k1Key() @@ -652,7 +650,7 @@ func newGsocTestWithOpts(t *testing.T, socId []byte, pingPeriod time.Duration, h gsoc := gsoc.New(log.NewLogger("test")) testutil.CleanupCloser(t, gsoc) - _, cl, listener, _ := newTestServer(t, testServerOptions{ + _, cl, listener, _, _ := newTestServer(t, testServerOptions{ Gsoc: gsoc, WsPath: fmt.Sprintf("/gsoc/subscribe/%s", hex.EncodeToString(chunkAddr.Bytes())), WsHeaders: headers, diff --git a/pkg/api/health_test.go b/pkg/api/health_test.go index 11c79f5366c..e0191f45602 100644 --- a/pkg/api/health_test.go +++ b/pkg/api/health_test.go @@ -19,7 +19,7 @@ func TestHealth(t *testing.T) { t.Run("probe not set", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{}) + testServer, _, _, _, _ := newTestServer(t, testServerOptions{}) // When probe is not set health endpoint should indicate that node is not healthy jsonhttptest.Request(t, testServer, http.MethodGet, "/health", http.StatusOK, jsonhttptest.WithExpectedJSONResponse(api.HealthStatusResponse{ @@ -33,7 +33,7 @@ func TestHealth(t *testing.T) { t.Parallel() probe := api.NewProbe() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ Probe: probe, }) diff --git a/pkg/api/logger_test.go b/pkg/api/logger_test.go index 78caf7078f3..8c7529d40e8 100644 --- a/pkg/api/logger_test.go +++ b/pkg/api/logger_test.go @@ -72,7 +72,7 @@ func TestGetLoggers(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) jsonhttptest.Request(t, client, http.MethodGet, "/loggers", http.StatusOK, jsonhttptest.WithUnmarshalJSONResponse(&have), ) @@ -88,7 +88,7 @@ func TestSetLoggerVerbosity(t *testing.T) { api.ReplaceLogSetVerbosityByExp(fn) }(api.LogSetVerbosityByExp) - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) type data struct { exp string @@ -143,7 +143,7 @@ func TestSetLoggerVerbosity(t *testing.T) { func Test_loggerGetHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -191,7 +191,7 @@ func Test_loggerGetHandler_invalidInputs(t *testing.T) { func Test_loggerSetVerbosityHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string diff --git a/pkg/api/p2p_test.go b/pkg/api/p2p_test.go index b4e60973ee9..68b9124b07d 100644 --- a/pkg/api/p2p_test.go +++ b/pkg/api/p2p_test.go @@ -39,7 +39,7 @@ func TestAddresses(t *testing.T) { ethereumAddress := common.HexToAddress("abcd") - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ PublicKey: privateKey.PublicKey, PSSPublicKey: pssPrivateKey.PublicKey, Overlay: overlay, @@ -78,7 +78,7 @@ func TestAddresses_error(t *testing.T) { testErr := errors.New("test error") - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithAddressesFunc(func() ([]multiaddr.Multiaddr, error) { return nil, testErr })), diff --git a/pkg/api/peer_test.go b/pkg/api/peer_test.go index 31544edc55b..bd0ee54e1a4 100644 --- a/pkg/api/peer_test.go +++ b/pkg/api/peer_test.go @@ -50,7 +50,7 @@ func TestConnect(t *testing.T) { t.Fatal(err) } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithConnectFunc(func(ctx context.Context, addrs []ma.Multiaddr) (*bzz.Address, error) { for _, addr := range addrs { if addr.String() == errorUnderlay { @@ -79,7 +79,7 @@ func TestConnect(t *testing.T) { }) t.Run("error - add peer", func(t *testing.T) { - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithConnectFunc(func(ctx context.Context, addrs []ma.Multiaddr) (*bzz.Address, error) { for _, addr := range addrs { if addr.String() == errorUnderlay { @@ -107,7 +107,7 @@ func TestDisconnect(t *testing.T) { errorAddress := swarm.MustParseHexAddress("ca1e9f3938cc1425c6061b96ad9eb93e134dfe8734ad490164ef20af9d1cf59a") testErr := errors.New("test error") - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithDisconnectFunc(func(addr swarm.Address, reason string) error { if reason != "user requested disconnect" { return testErr @@ -163,7 +163,7 @@ func TestPeer(t *testing.T) { t.Parallel() overlay := swarm.MustParseHexAddress("ca1e9f3938cc1425c6061b96ad9eb93e134dfe8734ad490164ef20af9d1cf59c") - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithPeersFunc(func() []p2p.Peer { return []p2p.Peer{{Address: overlay}} })), @@ -184,7 +184,7 @@ func TestBlocklistedPeers(t *testing.T) { t.Parallel() overlay := swarm.MustParseHexAddress("ca1e9f3938cc1425c6061b96ad9eb93e134dfe8734ad490164ef20af9d1cf59c") - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithBlocklistedPeersFunc(func() ([]p2p.BlockListedPeer, error) { return []p2p.BlockListedPeer{{Peer: p2p.Peer{Address: overlay}}}, nil })), @@ -201,7 +201,7 @@ func TestBlocklistedPeersErr(t *testing.T) { t.Parallel() overlay := swarm.MustParseHexAddress("ca1e9f3938cc1425c6061b96ad9eb93e134dfe8734ad490164ef20af9d1cf59c") - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithBlocklistedPeersFunc(func() ([]p2p.BlockListedPeer, error) { return []p2p.BlockListedPeer{{Peer: p2p.Peer{Address: overlay}}}, errors.New("some error") })), @@ -219,7 +219,7 @@ func TestBlocklistedPeersErr(t *testing.T) { func Test_peerConnectHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -254,7 +254,7 @@ func Test_peerConnectHandler_invalidInputs(t *testing.T) { func Test_peerDisconnectHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string diff --git a/pkg/api/pin_test.go b/pkg/api/pin_test.go index 009cae49afb..59b60bdc074 100644 --- a/pkg/api/pin_test.go +++ b/pkg/api/pin_test.go @@ -80,8 +80,8 @@ func checkPinHandlers(t *testing.T, client *http.Client, rootHash string, create // nolint:paralleltest func TestPinHandlers(t *testing.T) { var ( - storerMock = mockstorer.New() - client, _, _, _ = newTestServer(t, testServerOptions{ + storerMock = mockstorer.New() + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Post: mockpost.New(mockpost.WithAcceptAll()), }) @@ -151,7 +151,7 @@ func TestPinHandlers(t *testing.T) { func TestPinHandlersInvalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -205,7 +205,7 @@ func TestIntegrityHandler(t *testing.T) { t.Run("ok", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ PinIntegrity: &mockPinIntegrity{ Store: inmemstore.New(), tester: t, @@ -220,7 +220,7 @@ func TestIntegrityHandler(t *testing.T) { t.Run("wrong hash format", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ PinIntegrity: &mockPinIntegrity{ Store: inmemstore.New(), tester: t, diff --git a/pkg/api/pingpong_test.go b/pkg/api/pingpong_test.go index 20f8af6a06a..8872f655fab 100644 --- a/pkg/api/pingpong_test.go +++ b/pkg/api/pingpong_test.go @@ -38,7 +38,7 @@ func TestPingpong(t *testing.T) { return rtt, nil }) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ Pingpong: pingpongService, }) @@ -78,7 +78,7 @@ func TestPingpong(t *testing.T) { func Test_pingpongHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string diff --git a/pkg/api/postage_test.go b/pkg/api/postage_test.go index fd5db2cf666..86dc2c2e66a 100644 --- a/pkg/api/postage_test.go +++ b/pkg/api/postage_test.go @@ -64,7 +64,7 @@ func TestPostageCreateStamp(t *testing.T) { return txHash, batchID, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -88,7 +88,7 @@ func TestPostageCreateStamp(t *testing.T) { return common.Hash{}, nil, errors.New("err") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -108,7 +108,7 @@ func TestPostageCreateStamp(t *testing.T) { return common.Hash{}, nil, postagecontract.ErrInsufficientFunds }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -128,7 +128,7 @@ func TestPostageCreateStamp(t *testing.T) { return common.Hash{}, nil, postagecontract.ErrInvalidDepth }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -156,7 +156,7 @@ func TestPostageCreateStamp(t *testing.T) { return txHash, batchID, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -176,7 +176,7 @@ func TestPostageCreateStamp(t *testing.T) { t.Run("syncing in progress", func(t *testing.T) { t.Parallel() - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ SyncStatus: func() (bool, error) { return false, nil }, }) @@ -190,7 +190,7 @@ func TestPostageCreateStamp(t *testing.T) { t.Run("syncing failed", func(t *testing.T) { t.Parallel() - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ SyncStatus: func() (bool, error) { return true, errors.New("oops") }, }) @@ -216,7 +216,7 @@ func TestPostageGetStamps(t *testing.T) { t.Parallel() bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b)) - ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second}) jsonhttptest.Request(t, ts, http.MethodGet, "/stamps", http.StatusOK, jsonhttptest.WithExpectedJSONResponse(&api.PostageStampsResponse{ @@ -253,7 +253,7 @@ func TestPostageGetStamps(t *testing.T) { } ecs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(15), CurrentPrice: big.NewInt(12)} ebs := mock.New(mock.WithChainState(ecs)) - ts, _, _, _ := newTestServer(t, testServerOptions{Post: emp, BatchStore: ebs, BlockTime: 2 * time.Second}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{Post: emp, BatchStore: ebs, BlockTime: 2 * time.Second}) jsonhttptest.Request(t, ts, http.MethodGet, "/stamps/"+hex.EncodeToString(eb.ID), http.StatusNotFound, jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{ @@ -275,7 +275,7 @@ func TestGetAllBatches(t *testing.T) { mp := mockpost.New(mockpost.WithIssuer(si)) cs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(5), CurrentPrice: big.NewInt(2)} bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b)) - ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second}) oneBatch := struct { Batches []api.PostageBatchResponse `json:"batches"` @@ -312,7 +312,7 @@ func TestGetBatch(t *testing.T) { b.Value = big.NewInt(20) cs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(5), CurrentPrice: big.NewInt(2)} bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b)) - ts, _, _, _ := newTestServer(t, testServerOptions{BatchStore: bs, BlockTime: 2 * time.Second}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{BatchStore: bs, BlockTime: 2 * time.Second}) want := api.PostageBatchResponse{ BatchID: b.ID, @@ -367,7 +367,7 @@ func TestPostageGetStamp(t *testing.T) { mp := mockpost.New(mockpost.WithIssuer(si)) cs := &postage.ChainState{Block: 10, TotalAmount: big.NewInt(5), CurrentPrice: big.NewInt(2)} bs := mock.New(mock.WithChainState(cs), mock.WithBatch(b)) - ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{Post: mp, BatchStore: bs, BlockTime: 2 * time.Second}) t.Run("ok", func(t *testing.T) { t.Parallel() @@ -396,7 +396,7 @@ func TestPostageGetBuckets(t *testing.T) { si := postage.NewStampIssuer("", "", batchOk, big.NewInt(3), 11, 10, 1000, true) mp := mockpost.New(mockpost.WithIssuer(si)) - ts, _, _, _ := newTestServer(t, testServerOptions{Post: mp}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{Post: mp}) buckets := make([]api.BucketData, 1024) for i := range buckets { buckets[i] = api.BucketData{BucketID: uint32(i)} @@ -419,7 +419,7 @@ func TestPostageGetBuckets(t *testing.T) { t.Parallel() mpNotFound := mockpost.New() - tsNotFound, _, _, _ := newTestServer(t, testServerOptions{Post: mpNotFound}) + tsNotFound, _, _, _, _ := newTestServer(t, testServerOptions{Post: mpNotFound}) jsonhttptest.Request(t, tsNotFound, http.MethodGet, "/stamps/"+batchOkStr+"/buckets", http.StatusNotFound) }) @@ -434,7 +434,7 @@ func TestReserveState(t *testing.T) { s := mockstorer.New() s.SetStorageRadius(3) s.SetCommittedDepth(5) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ BatchStore: mock.New(mock.WithRadius(5)), Storer: s, }) @@ -449,7 +449,7 @@ func TestReserveState(t *testing.T) { t.Run("empty", func(t *testing.T) { t.Parallel() - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ BatchStore: mock.New(), Storer: mockstorer.New(), }) @@ -475,7 +475,7 @@ func TestChainState(t *testing.T) { return 17280, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ BatchStore: mock.New(mock.WithChainState(cs)), BackendOpts: []backendmock.Option{backendmock.WithBlockNumberFunc(func(ctx context.Context) (uint64, error) { return 1, nil @@ -505,7 +505,7 @@ func TestChainState(t *testing.T) { TotalAmount: big.NewInt(50), CurrentPrice: big.NewInt(5), } - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ BatchStore: mock.New(mock.WithChainState(cs)), BackendOpts: []backendmock.Option{backendmock.WithBlockNumberFunc(func(ctx context.Context) (uint64, error) { return 1, nil @@ -547,7 +547,7 @@ func TestPostageTopUpStamp(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -576,7 +576,7 @@ func TestPostageTopUpStamp(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -597,7 +597,7 @@ func TestPostageTopUpStamp(t *testing.T) { return common.Hash{}, errors.New("err") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -617,7 +617,7 @@ func TestPostageTopUpStamp(t *testing.T) { return common.Hash{}, postagecontract.ErrInsufficientFunds }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -640,7 +640,7 @@ func TestPostageTopUpStamp(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -677,7 +677,7 @@ func TestPostageDiluteStamp(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -706,7 +706,7 @@ func TestPostageDiluteStamp(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -727,7 +727,7 @@ func TestPostageDiluteStamp(t *testing.T) { return common.Hash{}, errors.New("err") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -747,7 +747,7 @@ func TestPostageDiluteStamp(t *testing.T) { return common.Hash{}, postagecontract.ErrInvalidDepth }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -770,7 +770,7 @@ func TestPostageDiluteStamp(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -886,7 +886,7 @@ func TestPostageAccessHandler(t *testing.T) { }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ PostageContract: contract, }) @@ -911,7 +911,7 @@ func TestPostageAccessHandler(t *testing.T) { func Test_postageCreateHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -961,7 +961,7 @@ func Test_postageCreateHandler_invalidInputs(t *testing.T) { func Test_postageGetStampBucketsHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -1022,7 +1022,7 @@ func Test_postageGetStampBucketsHandler_invalidInputs(t *testing.T) { func Test_postageGetStampHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -1084,7 +1084,7 @@ func Test_postageGetStampHandler_invalidInputs(t *testing.T) { func Test_postageTopUpHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -1163,7 +1163,7 @@ func Test_postageTopUpHandler_invalidInputs(t *testing.T) { func Test_postageDiluteHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -1250,7 +1250,7 @@ func TestPostageUpdateLabelStamp(t *testing.T) { si := postage.NewStampIssuer("original", "test identity", batchID, big.NewInt(3), 24, 6, 1000, false) mp := mockpost.New(mockpost.WithIssuer(si)) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ Post: mp, }) @@ -1265,7 +1265,7 @@ func TestPostageUpdateLabelStamp(t *testing.T) { t.Parallel() mp := mockpost.New() - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ Post: mp, }) @@ -1281,7 +1281,7 @@ func TestPostageUpdateLabelStamp(t *testing.T) { si := postage.NewStampIssuer("original", "test identity", batchID, big.NewInt(3), 24, 6, 1000, false) mp := mockpost.New(mockpost.WithIssuer(si)) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ Post: mp, }) @@ -1299,7 +1299,7 @@ func TestPostageUpdateLabelStamp(t *testing.T) { // must be rejected rather than silently blanking the issuer label. si := postage.NewStampIssuer("original", "test identity", batchID, big.NewInt(3), 24, 6, 1000, false) mp := mockpost.New(mockpost.WithIssuer(si)) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ Post: mp, }) @@ -1317,7 +1317,7 @@ func TestPostageUpdateLabelStamp(t *testing.T) { func Test_postageUpdateLabelHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string diff --git a/pkg/api/pss_test.go b/pkg/api/pss_test.go index d78f8ea668f..6bd048aa9d3 100644 --- a/pkg/api/pss_test.go +++ b/pkg/api/pss_test.go @@ -181,9 +181,9 @@ func TestPssSend(t *testing.T) { mtx.Unlock() return err } - mp = mockpost.New(mockpost.WithIssuer(postage.NewStampIssuer("", "", batchOk, big.NewInt(3), 11, 10, 1000, true))) - p = newMockPss(sendFn) - client, _, _, _ = newTestServer(t, testServerOptions{ + mp = mockpost.New(mockpost.WithIssuer(postage.NewStampIssuer("", "", batchOk, big.NewInt(3), 11, 10, 1000, true))) + p = newMockPss(sendFn) + client, _, _, _, _ = newTestServer(t, testServerOptions{ Pss: p, Storer: mockstorer.New(), Post: mp, @@ -382,7 +382,7 @@ func newPssTest(t *testing.T, o opts) (pss.Interface, *ecdsa.PublicKey, *websock if o.pingPeriod == 0 { o.pingPeriod = 10 * time.Second } - _, cl, listener, _ := newTestServer(t, testServerOptions{ + _, cl, listener, _, _ := newTestServer(t, testServerOptions{ Pss: pss, WsPath: "/pss/subscribe/testtopic", Storer: mockstorer.New(), @@ -396,7 +396,7 @@ func newPssTest(t *testing.T, o opts) (pss.Interface, *ecdsa.PublicKey, *websock func TestPssPostHandlerInvalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string diff --git a/pkg/api/readiness_test.go b/pkg/api/readiness_test.go index 0454f541c2f..7aa84a27733 100644 --- a/pkg/api/readiness_test.go +++ b/pkg/api/readiness_test.go @@ -18,7 +18,7 @@ func TestReadiness(t *testing.T) { t.Run("probe not set", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{}) + testServer, _, _, _, _ := newTestServer(t, testServerOptions{}) // When probe is not set readiness endpoint should indicate that API is not ready jsonhttptest.Request(t, testServer, http.MethodGet, "/readiness", http.StatusBadRequest) @@ -28,7 +28,7 @@ func TestReadiness(t *testing.T) { t.Parallel() probe := api.NewProbe() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ Probe: probe, }) diff --git a/pkg/api/redistribution_test.go b/pkg/api/redistribution_test.go index 879657c9e1d..cccc2383769 100644 --- a/pkg/api/redistribution_test.go +++ b/pkg/api/redistribution_test.go @@ -36,7 +36,7 @@ func TestRedistributionStatus(t *testing.T) { if err != nil { t.Errorf("redistribution put state: %v", err) } - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ StateStorer: store, TransactionOpts: []mock.Option{ mock.WithTransactionFeeFunc(func(ctx context.Context, txHash common.Hash) (*big.Int, error) { @@ -86,7 +86,7 @@ func TestRedistributionStatus(t *testing.T) { if err != nil { t.Errorf("redistribution put state: %v", err) } - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ StateStorer: store, TransactionOpts: []mock.Option{ mock.WithTransactionFeeFunc(func(ctx context.Context, txHash common.Hash) (*big.Int, error) { @@ -129,7 +129,7 @@ func TestRedistributionStatus(t *testing.T) { if err != nil { t.Errorf("redistribution put state: %v", err) } - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ StateStorer: store, TransactionOpts: []mock.Option{ mock.WithTransactionFeeFunc(func(ctx context.Context, txHash common.Hash) (*big.Int, error) { @@ -160,7 +160,7 @@ func TestRedistributionStatus(t *testing.T) { t.Run("bad request", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ BeeMode: api.LightMode, StateStorer: statestore.NewStateStore(), TransactionOpts: []mock.Option{ @@ -214,7 +214,7 @@ func TestRedistributionToggle(t *testing.T) { t.Run("patch false then true", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, redistributionTestOpts(t)) + srv, _, _, _, _ := newTestServer(t, redistributionTestOpts(t)) jsonhttptest.Request(t, srv, http.MethodPatch, "/redistributionstate", http.StatusOK, jsonhttptest.WithJSONRequestBody(map[string]any{"enabled": false}), @@ -245,7 +245,7 @@ func TestRedistributionToggle(t *testing.T) { t.Run("missing enabled", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, redistributionTestOpts(t)) + srv, _, _, _, _ := newTestServer(t, redistributionTestOpts(t)) jsonhttptest.Request(t, srv, http.MethodPatch, "/redistributionstate", http.StatusBadRequest, jsonhttptest.WithJSONRequestBody(map[string]any{}), jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ @@ -258,7 +258,7 @@ func TestRedistributionToggle(t *testing.T) { t.Run("null enabled", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, redistributionTestOpts(t)) + srv, _, _, _, _ := newTestServer(t, redistributionTestOpts(t)) jsonhttptest.Request(t, srv, http.MethodPatch, "/redistributionstate", http.StatusBadRequest, jsonhttptest.WithJSONRequestBody(map[string]any{"enabled": nil}), jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ @@ -271,7 +271,7 @@ func TestRedistributionToggle(t *testing.T) { t.Run("malformed json", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, redistributionTestOpts(t)) + srv, _, _, _, _ := newTestServer(t, redistributionTestOpts(t)) jsonhttptest.Request(t, srv, http.MethodPatch, "/redistributionstate", http.StatusBadRequest, jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "application/json"), jsonhttptest.WithRequestBody(bytes.NewReader([]byte("{invalid"))), @@ -285,7 +285,7 @@ func TestRedistributionToggle(t *testing.T) { t.Run("light mode", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ BeeMode: api.LightMode, StateStorer: statestore.NewStateStore(), }) @@ -301,7 +301,7 @@ func TestRedistributionToggle(t *testing.T) { t.Run("forbidden when agent missing", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ RedistributionAgentDisabled: true, }) jsonhttptest.Request(t, srv, http.MethodPatch, "/redistributionstate", http.StatusForbidden, @@ -316,7 +316,7 @@ func TestRedistributionToggle(t *testing.T) { t.Run("unavailable when full api disabled", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ FullAPIDisabled: true, }) jsonhttptest.Request(t, srv, http.MethodPatch, "/redistributionstate", http.StatusServiceUnavailable, diff --git a/pkg/api/router_test.go b/pkg/api/router_test.go index 4aaf1388aa2..f7a49dc27ae 100644 --- a/pkg/api/router_test.go +++ b/pkg/api/router_test.go @@ -411,7 +411,7 @@ func TestEndpointOptions(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, tc.serverOptions) + testServer, _, _, _, _ := newTestServer(t, tc.serverOptions) routeToName := func(route string) string { if route == "/" { diff --git a/pkg/api/settlements_test.go b/pkg/api/settlements_test.go index ae1f73439f8..d9964846fc6 100644 --- a/pkg/api/settlements_test.go +++ b/pkg/api/settlements_test.go @@ -38,7 +38,7 @@ func TestSettlements(t *testing.T) { return ret, err } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []mock.Option{mock.WithSettlementsSentFunc(settlementsSentFunc), mock.WithSettlementsRecvFunc(settlementsRecvFunc)}, }) @@ -87,7 +87,7 @@ func TestSettlementsError(t *testing.T) { settlementsSentFunc := func() (map[string]*big.Int, error) { return nil, wantErr } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []mock.Option{mock.WithSettlementsSentFunc(settlementsSentFunc)}, }) @@ -106,7 +106,7 @@ func TestSettlementsPeers(t *testing.T) { settlementSentFunc := func(swarm.Address) (*big.Int, error) { return big.NewInt(1000000000000000000), nil } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []mock.Option{mock.WithSettlementSentFunc(settlementSentFunc)}, }) @@ -133,7 +133,7 @@ func TestSettlementsPeersNoSettlements(t *testing.T) { t.Run("no sent", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []mock.Option{ mock.WithSettlementSentFunc(errFunc), mock.WithSettlementRecvFunc(noErrFunc), @@ -152,7 +152,7 @@ func TestSettlementsPeersNoSettlements(t *testing.T) { t.Run("no received", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []mock.Option{ mock.WithSettlementSentFunc(noErrFunc), mock.WithSettlementRecvFunc(errFunc), @@ -172,7 +172,7 @@ func TestSettlementsPeersNoSettlements(t *testing.T) { func Test_peerSettlementsHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -225,7 +225,7 @@ func TestSettlementsPeersError(t *testing.T) { settlementSentFunc := func(swarm.Address) (*big.Int, error) { return nil, wantErr } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ SwapOpts: []mock.Option{mock.WithSettlementSentFunc(settlementSentFunc)}, }) diff --git a/pkg/api/soc_test.go b/pkg/api/soc_test.go index 30b0295a9aa..21f5ff386d1 100644 --- a/pkg/api/soc_test.go +++ b/pkg/api/soc_test.go @@ -34,7 +34,7 @@ func TestSOC(t *testing.T) { mockStorer = mockstorer.New() ) t.Run("empty data", func(t *testing.T) { - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, @@ -57,7 +57,7 @@ func TestSOC(t *testing.T) { sig[12] = 0x98 sig[10] = 0x12 - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, @@ -74,7 +74,7 @@ func TestSOC(t *testing.T) { t.Run("ok", func(t *testing.T) { s := testingsoc.GenerateMockSOC(t, testData) - client, _, _, chanStore := newTestServer(t, testServerOptions{ + client, _, _, chanStore, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, @@ -127,7 +127,7 @@ func TestSOC(t *testing.T) { s := testingsoc.GenerateMockSOC(t, testData) t.Run("err - bad batch", func(t *testing.T) { hexbatch := "abcdefgg" - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, @@ -150,7 +150,7 @@ func TestSOC(t *testing.T) { t.Run("ok batch", func(t *testing.T) { s := testingsoc.GenerateMockSOC(t, testData) hexbatch := hex.EncodeToString(batchOk) - client, _, _, chanStorer := newTestServer(t, testServerOptions{ + client, _, _, chanStorer, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, @@ -169,7 +169,7 @@ func TestSOC(t *testing.T) { s := testingsoc.GenerateMockSOC(t, testData) hexbatch := hex.EncodeToString(batchOk) storer := mockstorer.New() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storer, Post: newTestPostService(), DirectUpload: true, @@ -193,7 +193,7 @@ func TestSOC(t *testing.T) { s := testingsoc.GenerateMockSOC(t, testData) hexbatch := hex.EncodeToString(batchOk) storer := mockstorer.New() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storer, Post: newTestPostService(), DirectUpload: true, @@ -222,10 +222,10 @@ func TestSOC(t *testing.T) { t.Parallel() var ( - s = testingsoc.GenerateMockSOC(t, testData) - storerMock = mockstorer.New() - batchStore = mockbatchstore.New() - client, _, _, _ = newTestServer(t, testServerOptions{ + s = testingsoc.GenerateMockSOC(t, testData) + storerMock = mockstorer.New() + batchStore = mockbatchstore.New() + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storerMock, BatchStore: batchStore, }) @@ -254,7 +254,7 @@ func TestSOC(t *testing.T) { t.Run("err - batch empty", func(t *testing.T) { s := testingsoc.GenerateMockSOC(t, testData) hexbatch := hex.EncodeToString(batchEmpty) - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockStorer, Post: newTestPostService(), DirectUpload: true, diff --git a/pkg/api/staking_test.go b/pkg/api/staking_test.go index febd9823bba..006a84d5eb3 100644 --- a/pkg/api/staking_test.go +++ b/pkg/api/staking_test.go @@ -39,7 +39,7 @@ func TestDepositStake(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodPost, depositStake(minStake), http.StatusOK) }) @@ -52,7 +52,7 @@ func TestDepositStake(t *testing.T) { return common.Hash{}, &staking.MinDepositError{Minimum: minDeposit} }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodPost, depositStake("1"), http.StatusBadRequest, jsonhttptest.WithExpectedJSONResponse(&api.StakeDepositErrorResponse{ Code: http.StatusBadRequest, @@ -69,7 +69,7 @@ func TestDepositStake(t *testing.T) { return common.Hash{}, staking.ErrInsufficientFunds }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodPost, depositStake(minStake), http.StatusBadRequest) jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "out of funds"}) }) @@ -82,7 +82,7 @@ func TestDepositStake(t *testing.T) { return common.Hash{}, fmt.Errorf("some error") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodPost, depositStake(minStake), http.StatusInternalServerError) jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "cannot stake"}) }) @@ -98,7 +98,7 @@ func TestDepositStake(t *testing.T) { return common.Hash{}, false, fmt.Errorf("update height failed") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodPost, depositStake(minStake), http.StatusOK, jsonhttptest.WithExpectedJSONResponse(&api.StakeTransactionReponse{TxHash: txHash.String()})) }) @@ -115,7 +115,7 @@ func TestDepositStake(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ StakingContract: contract, }) @@ -136,7 +136,7 @@ func TestGetStakeCommitted(t *testing.T) { return big.NewInt(1), nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodGet, "/stake", http.StatusOK, jsonhttptest.WithExpectedJSONResponse(&api.GetStakeResponse{ StakedAmount: bigint.Wrap(big.NewInt(1)), @@ -152,7 +152,7 @@ func TestGetStakeCommitted(t *testing.T) { return big.NewInt(0), fmt.Errorf("get stake failed") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contractWithError}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contractWithError}) jsonhttptest.Request(t, ts, http.MethodGet, "/stake", http.StatusInternalServerError, jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "get staked amount failed"})) }) @@ -168,7 +168,7 @@ func TestGetStakeCommitted(t *testing.T) { return nil, fmt.Errorf("get minimum deposit failed") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contractWithError}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contractWithError}) jsonhttptest.Request(t, ts, http.MethodGet, "/stake", http.StatusInternalServerError, jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "get minimum deposit failed"})) }) @@ -185,7 +185,7 @@ func TestGetStakeWithdrawable(t *testing.T) { return big.NewInt(1), nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodGet, "/stake/withdrawable", http.StatusOK, jsonhttptest.WithExpectedJSONResponse(&api.GetWithdrawableResponse{WithdrawableAmount: bigint.Wrap(big.NewInt(1))})) }) @@ -198,7 +198,7 @@ func TestGetStakeWithdrawable(t *testing.T) { return big.NewInt(0), fmt.Errorf("get stake failed") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contractWithError}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contractWithError}) jsonhttptest.Request(t, ts, http.MethodGet, "/stake/withdrawable", http.StatusInternalServerError, jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "get staked amount failed"})) }) @@ -207,7 +207,7 @@ func TestGetStakeWithdrawable(t *testing.T) { func Test_stakingDepositHandler_invalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string @@ -252,7 +252,7 @@ func TestWithdrawStake(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodDelete, "/stake/withdrawable", http.StatusOK, jsonhttptest.WithExpectedJSONResponse( &api.StakeTransactionReponse{TxHash: txHash.String()})) }) @@ -265,7 +265,7 @@ func TestWithdrawStake(t *testing.T) { return common.Hash{}, staking.ErrInsufficientStake }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodDelete, "/stake/withdrawable", http.StatusBadRequest, jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "insufficient stake to withdraw"})) }) @@ -278,7 +278,7 @@ func TestWithdrawStake(t *testing.T) { return common.Hash{}, fmt.Errorf("some error") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodDelete, "/stake/withdrawable", http.StatusInternalServerError) jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "cannot withdraw stake"}) }) @@ -295,7 +295,7 @@ func TestWithdrawStake(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ StakingContract: contract, }) @@ -318,7 +318,7 @@ func TestMigrateStake(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusOK, jsonhttptest.WithExpectedJSONResponse( &api.StakeTransactionReponse{TxHash: txHash.String()})) }) @@ -331,7 +331,7 @@ func TestMigrateStake(t *testing.T) { return common.Hash{}, staking.ErrInsufficientStake }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusBadRequest, jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "insufficient stake to migrate"})) }) @@ -344,7 +344,7 @@ func TestMigrateStake(t *testing.T) { return common.Hash{}, fmt.Errorf("some error") }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) + ts, _, _, _, _ := newTestServer(t, testServerOptions{StakingContract: contract}) jsonhttptest.Request(t, ts, http.MethodDelete, "/stake", http.StatusInternalServerError) jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{Code: http.StatusInternalServerError, Message: "cannot withdraw stake"}) }) @@ -361,7 +361,7 @@ func TestMigrateStake(t *testing.T) { return txHash, nil }), ) - ts, _, _, _ := newTestServer(t, testServerOptions{ + ts, _, _, _, _ := newTestServer(t, testServerOptions{ StakingContract: contract, }) diff --git a/pkg/api/status_test.go b/pkg/api/status_test.go index 9da5797caf8..5844e86547e 100644 --- a/pkg/api/status_test.go +++ b/pkg/api/status_test.go @@ -65,7 +65,7 @@ func TestGetStatus(t *testing.T) { statusSvc.SetSync(ssMock) - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ BeeMode: mode, NodeStatus: statusSvc, }) @@ -93,7 +93,7 @@ func TestGetStatusPeersIncludesBootnodes(t *testing.T) { statusSvc := status.NewService(log.Noop, nil, new(topologyPeersIterNoopMock), api.FullMode.String(), nil, nil, nil) - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ BeeMode: api.FullMode, NodeStatus: statusSvc, TopologyOpts: topoOpts, diff --git a/pkg/api/stewardship_test.go b/pkg/api/stewardship_test.go index 9b8831dd60b..6d2a1d93305 100644 --- a/pkg/api/stewardship_test.go +++ b/pkg/api/stewardship_test.go @@ -36,7 +36,7 @@ func TestStewardship(t *testing.T) { storer = mockstorer.New() addr = swarm.NewAddress([]byte{31: 128}) ) - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storer, Logger: logger, Steward: stewardMock, @@ -85,10 +85,10 @@ func TestStewardshipWithRedundancy(t *testing.T) { t.Parallel() var ( - storerMock = mockstorer.New() - localRetrieval = &localRetriever{getter: storerMock.ChunkStore()} - s = steward.New(storerMock, localRetrieval, storerMock.Cache()) - client, _, _, _ = newTestServer(t, testServerOptions{ + storerMock = mockstorer.New() + localRetrieval = &localRetriever{getter: storerMock.ChunkStore()} + s = steward.New(storerMock, localRetrieval, storerMock.Cache()) + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Logger: log.Noop, Steward: s, @@ -124,7 +124,7 @@ func TestStewardshipWithRedundancy(t *testing.T) { func TestStewardshipInvalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: mockstorer.New(), }) diff --git a/pkg/api/subdomain_test.go b/pkg/api/subdomain_test.go index c1f7fbed142..76f142b4ad4 100644 --- a/pkg/api/subdomain_test.go +++ b/pkg/api/subdomain_test.go @@ -92,10 +92,10 @@ func TestSubdomains(t *testing.T) { t.Parallel() var ( - dirUploadResource = "/bzz" - storer = mockstorer.New() - logger = log.Noop - client, _, _, _ = newTestServer(t, testServerOptions{ + dirUploadResource = "/bzz" + storer = mockstorer.New() + logger = log.Noop + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storer, Logger: logger, PreventRedirect: true, diff --git a/pkg/api/tag_test.go b/pkg/api/tag_test.go index 3a7697ddae8..834bd7a74e4 100644 --- a/pkg/api/tag_test.go +++ b/pkg/api/tag_test.go @@ -26,9 +26,9 @@ func tagsWithIdResource(id uint64) string { return fmt.Sprintf("/tags/%d", id) } // nolint:paralleltest func TestTags(t *testing.T) { var ( - tagsResource = "/tags" - storerMock = mockstorer.New() - client, _, _, _ = newTestServer(t, testServerOptions{ + tagsResource = "/tags" + storerMock = mockstorer.New() + client, _, _, _, _ = newTestServer(t, testServerOptions{ Storer: storerMock, Post: mockpost.New(mockpost.WithAcceptAll()), }) @@ -199,7 +199,7 @@ func TestTags(t *testing.T) { func TestTagsHandlersInvalidInputs(t *testing.T) { t.Parallel() - client, _, _, _ := newTestServer(t, testServerOptions{}) + client, _, _, _, _ := newTestServer(t, testServerOptions{}) tests := []struct { name string diff --git a/pkg/api/topology_test.go b/pkg/api/topology_test.go index 11dd6703791..2e2b34e4da9 100644 --- a/pkg/api/topology_test.go +++ b/pkg/api/topology_test.go @@ -14,7 +14,7 @@ import ( func TestTopologyOK(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{}) + testServer, _, _, _, _ := newTestServer(t, testServerOptions{}) var body []byte opts := jsonhttptest.WithPutResponseBody(&body) diff --git a/pkg/api/tracing_test.go b/pkg/api/tracing_test.go index f07fb5565c7..34430163320 100644 --- a/pkg/api/tracing_test.go +++ b/pkg/api/tracing_test.go @@ -44,7 +44,7 @@ func TestTracingHTTPSpan(t *testing.T) { t.Cleanup(func() { _ = tp.Shutdown(context.Background()) }) storerMock := mockstorer.New() - client, _, _, _ := newTestServer(t, testServerOptions{ + client, _, _, _, _ := newTestServer(t, testServerOptions{ Storer: storerMock, Tracer: tracing.NewTracerFromProvider(tp), Logger: log.Noop, diff --git a/pkg/api/transaction_test.go b/pkg/api/transaction_test.go index c23cc9cf3f5..d069ebb34d4 100644 --- a/pkg/api/transaction_test.go +++ b/pkg/api/transaction_test.go @@ -41,7 +41,7 @@ func TestTransactionStoredTransaction(t *testing.T) { t.Run("found", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) { return &transaction.StoredTransaction{ @@ -82,7 +82,7 @@ func TestTransactionStoredTransaction(t *testing.T) { t.Run("not found", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) { return nil, transaction.ErrUnknownTransaction @@ -100,7 +100,7 @@ func TestTransactionStoredTransaction(t *testing.T) { t.Run("other errors", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithStoredTransactionFunc(func(txHash common.Hash) (*transaction.StoredTransaction, error) { return nil, errors.New("err") @@ -151,7 +151,7 @@ func TestTransactionList(t *testing.T) { }, } - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithPendingTransactionsFunc(func() ([]common.Hash, error) { return []common.Hash{txHash1, txHash2}, nil @@ -205,7 +205,7 @@ func TestTransactionListError(t *testing.T) { t.Run("pending transactions error", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithPendingTransactionsFunc(func() ([]common.Hash, error) { return nil, errors.New("err") @@ -227,7 +227,7 @@ func TestTransactionListError(t *testing.T) { t.Run("pending transactions error", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithPendingTransactionsFunc(func() ([]common.Hash, error) { return []common.Hash{txHash1}, nil @@ -254,7 +254,7 @@ func TestTransactionResend(t *testing.T) { t.Run("ok", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithResendTransactionFunc(func(ctx context.Context, txHash common.Hash) error { return nil @@ -272,7 +272,7 @@ func TestTransactionResend(t *testing.T) { t.Run("unknown transaction", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithResendTransactionFunc(func(ctx context.Context, txHash common.Hash) error { return transaction.ErrUnknownTransaction @@ -291,7 +291,7 @@ func TestTransactionResend(t *testing.T) { t.Run("already imported", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithResendTransactionFunc(func(ctx context.Context, txHash common.Hash) error { return transaction.ErrAlreadyImported @@ -310,7 +310,7 @@ func TestTransactionResend(t *testing.T) { t.Run("other error", func(t *testing.T) { t.Parallel() - testServer, _, _, _ := newTestServer(t, testServerOptions{ + testServer, _, _, _, _ := newTestServer(t, testServerOptions{ TransactionOpts: []mock.Option{ mock.WithResendTransactionFunc(func(ctx context.Context, txHash common.Hash) error { return errors.New("err") diff --git a/pkg/api/wallet_test.go b/pkg/api/wallet_test.go index c8bf92b1c01..7dfe45e7c7e 100644 --- a/pkg/api/wallet_test.go +++ b/pkg/api/wallet_test.go @@ -27,7 +27,7 @@ func TestWallet(t *testing.T) { t.Run("Okay", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ Erc20Opts: []erc20mock.Option{ erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) { return big.NewInt(10000000000000000), nil @@ -52,7 +52,7 @@ func TestWallet(t *testing.T) { t.Run("500 - erc20 error", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ BackendOpts: []backendmock.Option{ backendmock.WithBalanceAt(func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { return new(big.Int), nil @@ -70,7 +70,7 @@ func TestWallet(t *testing.T) { t.Run("500 - chain backend error", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ Erc20Opts: []erc20mock.Option{ erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) { return new(big.Int), nil @@ -88,7 +88,7 @@ func TestWallet(t *testing.T) { t.Run("swap disabled", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ SwapDisabled: true, Erc20Opts: []erc20mock.Option{ erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) { @@ -114,7 +114,7 @@ func TestWallet(t *testing.T) { t.Run("erc20 service unavailable", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ SwapDisabled: true, Erc20ServiceNil: true, BackendOpts: []backendmock.Option{ @@ -136,7 +136,7 @@ func TestWallet(t *testing.T) { t.Run("chequebook disabled", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ ChequebookDisabled: true, Erc20Opts: []erc20mock.Option{ erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) { @@ -166,7 +166,7 @@ func TestWalletWithdraw(t *testing.T) { t.Run("address not whitelisted", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{}) + srv, _, _, _, _ := newTestServer(t, testServerOptions{}) jsonhttptest.Request(t, srv, http.MethodPost, "/wallet/withdraw/BZZ?address=0xaf&amount=99999999", http.StatusBadRequest, jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ @@ -178,7 +178,7 @@ func TestWalletWithdraw(t *testing.T) { t.Run("invalid coin type", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{}) + srv, _, _, _, _ := newTestServer(t, testServerOptions{}) jsonhttptest.Request(t, srv, http.MethodPost, "/wallet/withdraw/BTC?address=0xaf&amount=99999999", http.StatusBadRequest, jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ @@ -190,7 +190,7 @@ func TestWalletWithdraw(t *testing.T) { t.Run("BZZ erc20 balance error", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ WhitelistedAddr: "0xaf", }) @@ -204,7 +204,7 @@ func TestWalletWithdraw(t *testing.T) { t.Run("BZZ erc20 balance insufficient", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ WhitelistedAddr: "0xaf", Erc20Opts: []erc20mock.Option{ erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) { @@ -223,7 +223,7 @@ func TestWalletWithdraw(t *testing.T) { t.Run("BZZ erc20 transfer error", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ WhitelistedAddr: "0xaf", Erc20Opts: []erc20mock.Option{ erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) { @@ -244,7 +244,7 @@ func TestWalletWithdraw(t *testing.T) { txHash := common.HexToHash("0x00f") - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ WhitelistedAddr: "0xaf", Erc20Opts: []erc20mock.Option{ erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) { @@ -271,7 +271,7 @@ func TestWalletWithdraw(t *testing.T) { t.Run("native balance error", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ WhitelistedAddr: "0xaf", }) @@ -285,7 +285,7 @@ func TestWalletWithdraw(t *testing.T) { t.Run("native insufficient balance", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ WhitelistedAddr: "0xaf", BackendOpts: []backendmock.Option{ backendmock.WithBalanceAt(func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { @@ -304,7 +304,7 @@ func TestWalletWithdraw(t *testing.T) { t.Run("native backend send error", func(t *testing.T) { t.Parallel() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ WhitelistedAddr: "0xaf", BackendOpts: []backendmock.Option{ backendmock.WithBalanceAt(func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { @@ -325,7 +325,7 @@ func TestWalletWithdraw(t *testing.T) { txHash := common.HexToHash("0x00f") - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ WhitelistedAddr: "0xaf", BackendOpts: []backendmock.Option{ backendmock.WithBalanceAt(func(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) { diff --git a/pkg/api/welcome_message_test.go b/pkg/api/welcome_message_test.go index 4127f8e3e43..92a5ad1a3c3 100644 --- a/pkg/api/welcome_message_test.go +++ b/pkg/api/welcome_message_test.go @@ -22,7 +22,7 @@ func TestGetWelcomeMessage(t *testing.T) { const DefaultTestWelcomeMessage = "Hello World!" - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithGetWelcomeMessageFunc(func() string { return DefaultTestWelcomeMessage })), @@ -74,7 +74,7 @@ func TestSetWelcomeMessage(t *testing.T) { t.Run(tC.desc, func(t *testing.T) { mockP2P := mock.New() - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mockP2P, }) @@ -110,7 +110,7 @@ func TestSetWelcomeMessageInternalServerError(t *testing.T) { testError := errors.New("Could not set value") testURL := "/welcome-message" - srv, _, _, _ := newTestServer(t, testServerOptions{ + srv, _, _, _, _ := newTestServer(t, testServerOptions{ P2P: mock.New(mock.WithSetWelcomeMessageFunc(func(string) error { return testError })), From 0451a8d8db1454cd6e69653ace21338866ad201e Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 11:36:38 +0200 Subject: [PATCH 23/33] test: enhance gsoc pipe test for subscription handling and message queuing --- pkg/api/gsoc_test.go | 93 ++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 51 deletions(-) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index e1d74099b5c..105aa89f110 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -279,16 +279,10 @@ func TestGsocWebsocketInvalidFieldsHeader(t *testing.T) { } // TestGsocWebsocketSlowConsumer verifies that a subscriber that falls behind -// incoming GSOC messages is not dropped or disconnected: the server queues -// every message (no cap) and delivers the full backlog, in order, once the -// consumer catches up, instead of racing on the underlying websocket -// connection or blocking the (synchronous) GSOC handler indefinitely. -// -// The connection is served over an in-memory net.Pipe, which is fully -// synchronous (unbuffered): a write only completes once a matching read -// consumes it, so the server-side writer is deterministically blocked on the -// first message for as long as the client doesn't read, while further -// messages still queue up behind it without being dropped. +// incoming GSOC messages is not dropped or disconnected: the server queues the +// messages and delivers the backlog, in order, once the consumer catches up, +// instead of racing on the underlying websocket connection or blocking the +// (synchronous) GSOC handler indefinitely. func TestGsocWebsocketSlowConsumer(t *testing.T) { t.Parallel() @@ -406,17 +400,19 @@ func TestGsocQueueRelease(t *testing.T) { // newGsocPipeTest subscribes to the GSOC address of socID over an in-memory // net.Pipe instead of a real socket, so that a test fully controls when the -// client reads: writes to a pipe block until the other side reads, which is -// what makes the server side queue observable. It returns the listener to -// publish through, the client end of the subscription and the signer owning -// the subscribed address. -func newGsocPipeTest(t *testing.T, socID []byte) (gsoc.Listener, *websocket.Conn, crypto.Signer) { +// client reads: a pipe is unbuffered, so a write only completes once the other +// side reads it, which pins the server side writer on the first message and +// makes the queue behind it observable. A real socket would absorb the whole +// backlog in its kernel buffers instead. It returns once the subscription is +// registered, handing back the listener to publish through, the client end of +// the subscription and the signer owning the subscribed address. +func newGsocPipeTest(t *testing.T, socID []byte) (*subscribedListener, *websocket.Conn, crypto.Signer) { t.Helper() var ( batchStore = mockbatchstore.New() storer = mockstorer.New() - gsocSvc = gsoc.New(log.Noop) + gsocSvc = newSubscribedListener(gsoc.New(log.Noop)) ) testutil.CleanupCloser(t, gsocSvc) @@ -456,49 +452,44 @@ func newGsocPipeTest(t *testing.T, socID []byte) (gsoc.Listener, *websocket.Conn } testutil.CleanupCloser(t, cl) - // Subscribe runs synchronously in the HTTP handler before the connection - // is handed off to its own goroutine, but Dial returning only means the - // handshake bytes were exchanged over the pipe — it is not a - // happens-before guarantee that the server has gone on to call - // Subscribe. Those are two independent continuations of the same - // rendezvous, and either goroutine can be scheduled first; a message - // sent right after Dial can race Subscribe and, if it loses, is dropped - // before any handler exists to queue it — permanently, not just - // delayed. Real network I/O (as in TestGsocWebsocketMessageOrdering) - // involves enough actual syscalls to force scheduler yields that this is - // a non-issue in practice, but the in-memory pipe used here removes that - // cushion. - // - // Give the server's goroutine time to reach Subscribe before sending - // anything: this can't be done by retrying a read with a short deadline - // instead, because gorilla/websocket permanently poisons a *Conn for all - // further reads once any read — including one that merely times out — - // returns an error, so a read that might legitimately need a retry - // cannot be attempted more than once on the same connection. - time.Sleep(300 * time.Millisecond) - - // Confirm the subscription is actually active (rather than silently - // trusting the sleep) with a single throwaway round-trip. - warmup := []byte{0xff} - { - ch, _ := cac.New(warmup) - socCh := soc.New(socID, ch) - signedCh, _ := socCh.Sign(signer) - socCh, _ = soc.FromChunk(signedCh) - gsocSvc.Handle(socCh) + // Dial returning only means the handshake bytes were exchanged over the + // pipe; it says nothing about how far the server side handler has got. + // Publishing before it reaches Subscribe would lose the message for good + // — there is no subscriber to queue it — so wait for the registration + // itself. + select { + case <-gsocSvc.subscribed: + case <-time.After(longTimeout): + t.Fatal("timed out waiting for the gsoc subscription") } + if err := cl.SetReadDeadline(time.Now().Add(longTimeout)); err != nil { t.Fatal(err) } - if _, got, err := cl.ReadMessage(); err != nil { - t.Fatalf("warmup message: %v", err) - } else if !bytes.Equal(got, warmup) { - t.Fatalf("warmup message: got %q, want %q", got, warmup) - } return gsocSvc, cl, signer } +// subscribedListener reports the completion of the first Subscribe call on a +// channel. The GSOC listener offers no readiness signal of its own, and the +// websocket handshake is not one either: a test that published as soon as Dial +// returned would race the server goroutine's continuation into Subscribe. +type subscribedListener struct { + gsoc.Listener + subscribed chan struct{} + once sync.Once +} + +func newSubscribedListener(l gsoc.Listener) *subscribedListener { + return &subscribedListener{Listener: l, subscribed: make(chan struct{})} +} + +func (l *subscribedListener) Subscribe(address swarm.Address, handler gsoc.Handler) func() { + cleanup := l.Listener.Subscribe(address, handler) + l.once.Do(func() { close(l.subscribed) }) + return cleanup +} + // pipeListener is a net.Listener that hands out pre-established net.Conn // pairs, so an http.Server can be driven over an in-memory net.Pipe instead // of a real OS socket. From 351824c45eda16ee018a9b59c8246335f3b27edd Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 11:54:28 +0200 Subject: [PATCH 24/33] test: enhance GSOC websocket tests for SOC fields and stalled consumer behavior --- pkg/api/gsoc_test.go | 100 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 105aa89f110..32d9e2b2c3f 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -12,6 +12,7 @@ import ( "net" "net/http" "net/url" + "slices" "strings" "sync" "testing" @@ -174,14 +175,19 @@ func TestGsocWebsocketWrappedChunkData(t *testing.T) { } } -// TestGsocWebsocketSocFields verifies that multiple SOC fields are serialized in -// the order they are provided in the Swarm-Soc-Fields header. +// TestGsocWebsocketSocFields verifies that every SOC field can be requested +// through the Swarm-Soc-Fields header, that field names are case insensitive +// and that the fields are serialized in the order they are listed in the +// header rather than in any order internal to the node. Every expected field +// is derived from the signer and the wrapped chunk, so the recovered public +// key in particular is checked against an independently compressed key rather +// than against whatever the SOC happens to carry. func TestGsocWebsocketSocFields(t *testing.T) { t.Parallel() var ( id = make([]byte, 32) - headers = http.Header{api.SwarmSocFieldsHeader: []string{"identifier,wrappedAddress,payload"}} + headers = http.Header{api.SwarmSocFieldsHeader: []string{"payload,address,recoveredPubKey,identifier,signature,wrappedAddress,span"}} g, cl, signer, _, _ = newGsocTestWithOpts(t, id, 0, headers) respC = make(chan error, 1) payload = []byte("The future is already here — it's just not evenly distributed.") @@ -199,10 +205,29 @@ func TestGsocWebsocketSocFields(t *testing.T) { socCh, _ = soc.FromChunk(signedCh) g.Handle(socCh) - expected := make([]byte, 0, len(id)+swarm.HashSize+len(payload)) - expected = append(expected, id...) - expected = append(expected, ch.Address().Bytes()...) - expected = append(expected, payload...) + pubKey, err := signer.PublicKey() + if err != nil { + t.Fatal(err) + } + owner, err := signer.EthereumAddress() + if err != nil { + t.Fatal(err) + } + socAddr, err := soc.CreateAddress(id, owner.Bytes()) + if err != nil { + t.Fatal(err) + } + + expected := slices.Concat( + payload, + socAddr.Bytes(), + crypto.EncodeSecp256k1PublicKey(pubKey), + id, + // the signature is what follows the identifier in the signed chunk + signedCh.Data()[swarm.HashSize:swarm.HashSize+swarm.SocSignatureSize], + ch.Address().Bytes(), + ch.Data()[:swarm.SpanSize], + ) go expectMessage(t, cl, respC, expected) if err := <-respC; err != nil { @@ -377,6 +402,38 @@ func TestGsocWebsocketQueueBound(t *testing.T) { } } +// TestGsocWebsocketStalledConsumer verifies the failure mode of a subscriber +// that never reads: the writer cannot hand its message over, so the write +// deadline fires and it gives up on the connection. On its way out it closes +// the connection, unsubscribes from the GSOC address so that nothing can queue +// further messages, and releases the backlog nothing will ever drain (see +// TestGsocQueueRelease for what release itself drops). The test therefore runs +// for as long as the write deadline. +func TestGsocWebsocketStalledConsumer(t *testing.T) { + t.Parallel() + + id := make([]byte, 32) + gsocSvc, cl, signer := newGsocPipeTest(t, id) + + // a single message suffices: the pipe is unbuffered, so this one write + // pins the writer until its deadline expires. + ch, _ := cac.New([]byte("nobody is going to read this")) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + gsocSvc.Handle(socCh) + + select { + case <-gsocSvc.unsubscribed: + case <-time.After(longTimeout): + t.Fatal("the writer did not give up on a consumer that never reads") + } + + if _, _, err := cl.ReadMessage(); err == nil { + t.Fatal("read a message off a connection the writer gave up on, want it closed") + } +} + // TestGsocQueueRelease verifies that the backlog of a subscription whose // writer is gone is discarded, rather than kept around by a producer that is // still mid-callback when the connection is torn down. @@ -470,24 +527,35 @@ func newGsocPipeTest(t *testing.T, socID []byte) (*subscribedListener, *websocke return gsocSvc, cl, signer } -// subscribedListener reports the completion of the first Subscribe call on a -// channel. The GSOC listener offers no readiness signal of its own, and the -// websocket handshake is not one either: a test that published as soon as Dial -// returned would race the server goroutine's continuation into Subscribe. +// subscribedListener reports the completion of the first Subscribe call, and +// of the cleanup that ends it, on channels. The GSOC listener offers no +// readiness signal of its own, and the websocket handshake is not one either: +// a test that published as soon as Dial returned would race the server +// goroutine's continuation into Subscribe. The cleanup signal is the other end +// of that: it is how a test observes the writer giving up on a connection. type subscribedListener struct { gsoc.Listener - subscribed chan struct{} - once sync.Once + subscribed chan struct{} + unsubscribed chan struct{} + subOnce sync.Once + unsubOnce sync.Once } func newSubscribedListener(l gsoc.Listener) *subscribedListener { - return &subscribedListener{Listener: l, subscribed: make(chan struct{})} + return &subscribedListener{ + Listener: l, + subscribed: make(chan struct{}), + unsubscribed: make(chan struct{}), + } } func (l *subscribedListener) Subscribe(address swarm.Address, handler gsoc.Handler) func() { cleanup := l.Listener.Subscribe(address, handler) - l.once.Do(func() { close(l.subscribed) }) - return cleanup + l.subOnce.Do(func() { close(l.subscribed) }) + return func() { + cleanup() + l.unsubOnce.Do(func() { close(l.unsubscribed) }) + } } // pipeListener is a net.Listener that hands out pre-established net.Conn From 1a57d6cc10f3bb43b5406fd5d12f30923a39a98d Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 13:10:58 +0200 Subject: [PATCH 25/33] refactor: update test timeout settings and adjust batch depth in api tests --- Makefile | 11 +++++++---- pkg/api/api_test.go | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 645cbb31b5f..b8968c3a584 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,9 @@ REACHABILITY_OVERRIDE_PUBLIC ?= false BATCHFACTOR_OVERRIDE_PUBLIC ?= 5 BEE_IMAGE ?= ethersphere/bee:latest PLATFORM ?= linux/amd64 +# go test defaults to 10m per package binary, which the pkg/api suite exceeds +# under the race detector on the slower CI runners. +TEST_TIMEOUT ?= 30m BEE_API_VERSION ?= "$(shell grep '^ version:' openapi/Swarm.yaml | awk '{print $$2}')" @@ -99,9 +102,9 @@ check-whitespace: .PHONY: test-race test-race: ifdef cover - $(GO) test -race -failfast -coverprofile=cover.out -v ./... + $(GO) test -race -failfast -timeout $(TEST_TIMEOUT) -coverprofile=cover.out -v ./... else - $(GO) test -race -failfast -v ./... + $(GO) test -race -failfast -timeout $(TEST_TIMEOUT) -v ./... endif .PHONY: test-integration @@ -127,9 +130,9 @@ endif .PHONY: test-ci-race test-ci-race: ifdef cover - $(GO) test -race -coverprofile=cover.out ./... + $(GO) test -race -timeout $(TEST_TIMEOUT) -coverprofile=cover.out ./... else - $(GO) test -race ./... + $(GO) test -race -timeout $(TEST_TIMEOUT) ./... endif .PHONY: build diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index b31459cc2ad..8b0fc2eebf3 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -803,7 +803,11 @@ func newTestPostService() postage.Service { "", batchOk, big.NewInt(3), - 11, + // batch depth over bucket depth: uploads here are a handful of + // chunks with random addresses, and an immutable batch fails the + // whole request once one bucket is full, so leave enough room per + // bucket for several of them to collide. + 16, 10, 1000, true, From 043d7945c4608c0e2baec0a2b70ff07dea36c814 Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 13:34:22 +0200 Subject: [PATCH 26/33] test: optimize chunk building in GSOC websocket tests for performance --- pkg/api/gsoc_test.go | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 32d9e2b2c3f..8c325b71b97 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -316,17 +316,24 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { id := make([]byte, 32) gsocSvc, cl, signer := newGsocPipeTest(t, id) + // Build the chunks before publishing any of them: from the first publish + // until the consumer starts reading again the server's writer is blocked + // on the pipe against its write deadline, so only the publishing loop + // belongs inside that window. + payloads := make([][]byte, messageCount) + socs := make([]*soc.SOC, messageCount) + for i := range socs { + payloads[i] = []byte{byte(i)} + ch, _ := cac.New(payloads[i]) + signedCh, _ := soc.New(id, ch).Sign(signer) + socs[i], _ = soc.FromChunk(signedCh) + } + // never read from cl while queuing every message: the first message // blocks the single writer goroutine (nothing reads the pipe yet), and // the rest pile up behind it in the queue. messageCount is well below // api.GsocQueueCapacity, so none of them is evicted. - payloads := make([][]byte, messageCount) - for i := range messageCount { - payloads[i] = []byte{byte(i)} - ch, _ := cac.New(payloads[i]) - socCh := soc.New(id, ch) - signedCh, _ := socCh.Sign(signer) - socCh, _ = soc.FromChunk(signedCh) + for _, socCh := range socs { gsocSvc.Handle(socCh) } @@ -355,16 +362,24 @@ func TestGsocWebsocketQueueBound(t *testing.T) { id := make([]byte, 32) gsocSvc, cl, signer := newGsocPipeTest(t, id) + // Build every chunk before publishing any of them. Signing and recovering + // a few hundred single owner chunks is the expensive part of this test, + // and from the first publish until the consumer starts reading again the + // server's writer sits blocked on the pipe, against a write deadline that + // closes the connection if it is not drained in time. Only the publishing + // loop belongs inside that window. + socs := make([]*soc.SOC, messageCount) + for i := range socs { + ch, _ := cac.New([]byte{byte(i >> 8), byte(i)}) + signedCh, _ := soc.New(id, ch).Sign(signer) + socs[i], _ = soc.FromChunk(signedCh) + } + // never read from cl while queuing every message: the first message // blocks the single writer goroutine (nothing reads the pipe yet), and // the rest pile up behind it until the queue is full and starts dropping // its oldest entries. - for i := range messageCount { - payload := []byte{byte(i >> 8), byte(i)} - ch, _ := cac.New(payload) - socCh := soc.New(id, ch) - signedCh, _ := socCh.Sign(signer) - socCh, _ = soc.FromChunk(signedCh) + for _, socCh := range socs { gsocSvc.Handle(socCh) } From 485b65c0c2d405613669a137a0f4932c90e47cee Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 13:45:26 +0200 Subject: [PATCH 27/33] refactor: improve websocket shutdown handling --- pkg/api/gsoc.go | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index a780078c9dd..c7f168e62fd 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -323,6 +323,23 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, cleanup func(), queue *g select { case <-wake: for { + // Draining a backlog must not outlast the node. A consumer + // that keeps every write just under the write deadline makes + // each message cost seconds, so a full queue would otherwise + // hold this goroutine for minutes: long past the second that + // Close waits for it, leaving the shutdown to report open + // websockets and starving the keepalive ping in the meantime. + // Re-check the exits between messages to bound that to the + // single write already in flight. + select { + case <-s.quit: + s.gsocWsNotifyClose(conn) + return + case <-gone: + return + default: + } + b, ok := queue.pop() if !ok { break @@ -347,15 +364,7 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, cleanup func(), queue *g case <-s.quit: // shutdown - err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) - if err != nil { - s.logger.Debug("gsoc ws: set write deadline failed", "error", err) - return - } - err = conn.WriteMessage(websocket.CloseMessage, []byte{}) - if err != nil { - s.logger.Debug("gsoc ws: write close message failed", "error", err) - } + s.gsocWsNotifyClose(conn) return case <-gone: // client gone @@ -373,3 +382,15 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, cleanup func(), queue *g } } } + +// gsocWsNotifyClose tells the subscriber that the node is going away. It is +// best effort: the connection is closed either way once the writer returns. +func (s *Service) gsocWsNotifyClose(conn *websocket.Conn) { + if err := conn.SetWriteDeadline(time.Now().Add(writeDeadline)); err != nil { + s.logger.Debug("gsoc ws: set write deadline failed", "error", err) + return + } + if err := conn.WriteMessage(websocket.CloseMessage, []byte{}); err != nil { + s.logger.Debug("gsoc ws: write close message failed", "error", err) + } +} From 6831a2cd8ed9748019162f273d8985bbc6998fbc Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 13:49:50 +0200 Subject: [PATCH 28/33] test: add concurrent subscription handling --- pkg/gsoc/gsoc.go | 20 ++++++++++++---- pkg/gsoc/gsoc_test.go | 54 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/pkg/gsoc/gsoc.go b/pkg/gsoc/gsoc.go index f4ad79a6ccc..39b21d2c635 100644 --- a/pkg/gsoc/gsoc.go +++ b/pkg/gsoc/gsoc.go @@ -5,6 +5,7 @@ package gsoc import ( + "slices" "sync" "github.com/ethersphere/bee/v2/pkg/log" @@ -40,20 +41,28 @@ func New(logger log.Logger) Listener { } // Subscribe allows the definition of a Handler func on a specific GSOC address. +// +// Handle iterates the handlers of an address without holding handlersMu, so a +// slice that has been handed out must never be written to again. Subscribing +// and unsubscribing therefore publish a new slice instead of appending to, or +// shifting elements within, the backing array a concurrent Handle may be +// reading. func (l *listener) Subscribe(address swarm.Address, handler Handler) (cleanup func()) { + key := address.ByteString() + l.handlersMu.Lock() defer l.handlersMu.Unlock() - l.handlers[address.ByteString()] = append(l.handlers[address.ByteString()], &handler) + l.handlers[key] = append(slices.Clone(l.handlers[key]), &handler) return func() { l.handlersMu.Lock() defer l.handlersMu.Unlock() - h := l.handlers[address.ByteString()] + h := l.handlers[key] for i := range h { if h[i] == &handler { - l.handlers[address.ByteString()] = append(h[:i], h[i+1:]...) + l.handlers[key] = slices.Delete(slices.Clone(h), i, i+1) return } } @@ -67,7 +76,7 @@ func (l *listener) Handle(c *soc.SOC) { return // no handler } h := l.getHandlers(addr) - if h == nil { + if len(h) == 0 { return // no handler } l.logger.Debug("new incoming GSOC message", "GSOC Address", addr, "wrapped chunk address", c.WrappedChunk().Address()) @@ -77,6 +86,9 @@ func (l *listener) Handle(c *soc.SOC) { } } +// getHandlers returns the handlers currently subscribed to address. The +// returned slice is shared with the subscription bookkeeping and must only be +// read, see Subscribe. func (p *listener) getHandlers(address swarm.Address) []*Handler { p.handlersMu.Lock() defer p.handlersMu.Unlock() diff --git a/pkg/gsoc/gsoc_test.go b/pkg/gsoc/gsoc_test.go index 2ad61fff16d..8ad2a8df410 100644 --- a/pkg/gsoc/gsoc_test.go +++ b/pkg/gsoc/gsoc_test.go @@ -5,6 +5,7 @@ package gsoc_test import ( + "sync" "testing" "github.com/ethersphere/bee/v2/pkg/cac" @@ -89,3 +90,56 @@ func ensureCalls(t *testing.T, calls *int, exp int) { t.Fatalf("expected %d calls, found %d", exp, *calls) } } + +// TestConcurrentSubscribeHandle verifies that subscriptions coming and going +// while messages are handled is safe: Handle iterates the handlers of an +// address without holding the lock, so a subscription ending concurrently must +// not mutate the slice it is iterating. Run with -race to be meaningful. +func TestConcurrentSubscribeHandle(t *testing.T) { + t.Parallel() + + const ( + goroutines = 4 + iterations = 200 + ) + + var ( + g = gsoc.New(log.Noop) + socID = testutil.RandBytes(t, 32) + privKey, _ = crypto.GenerateSecp256k1Key() + signer = crypto.NewDefaultSigner(privKey) + owner, _ = signer.EthereumAddress() + address, _ = soc.CreateAddress(socID, owner.Bytes()) + noop = func(*soc.SOC) {} + ) + + ch, _ := cac.New([]byte("Hello there!")) + socCh := soc.New(socID, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + + // a subscription that outlives the test, so that Handle always has + // handlers to iterate over. + cleanup := g.Subscribe(address, noop) + defer cleanup() + + var wg sync.WaitGroup + for range goroutines { + wg.Add(1) + go func() { + defer wg.Done() + for range iterations { + g.Subscribe(address, noop)() + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + for range iterations { + g.Handle(socCh) + } + }() + } + wg.Wait() +} From 2ea522b9106ec6f801a5b3970b4f15e941914289 Mon Sep 17 00:00:00 2001 From: nugaon Date: Tue, 22 Sep 2026 13:53:11 +0200 Subject: [PATCH 29/33] refactor: improve handler cleanup logic in subscription --- pkg/gsoc/gsoc.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/gsoc/gsoc.go b/pkg/gsoc/gsoc.go index 39b21d2c635..bf9bd09e013 100644 --- a/pkg/gsoc/gsoc.go +++ b/pkg/gsoc/gsoc.go @@ -62,7 +62,14 @@ func (l *listener) Subscribe(address swarm.Address, handler Handler) (cleanup fu h := l.handlers[key] for i := range h { if h[i] == &handler { - l.handlers[key] = slices.Delete(slices.Clone(h), i, i+1) + if len(h) == 1 { + // drop the entry with its last subscriber, so that + // addresses subscribed to briefly do not accumulate in + // the map for the lifetime of the node. + delete(l.handlers, key) + } else { + l.handlers[key] = slices.Delete(slices.Clone(h), i, i+1) + } return } } From 154b4d27c150e3fa9feb557d238b6b5d124e2d9e Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 23 Sep 2026 17:16:50 +0200 Subject: [PATCH 30/33] refactor: enhance background context handling for websocket caching --- pkg/api/api.go | 8 ++++++++ pkg/api/gsoc.go | 22 +++++++++++++++------- pkg/api/gsoc_test.go | 14 ++++++++++++-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 84735cf1f7a..2c53748dbb8 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -182,6 +182,12 @@ type Service struct { wsWg sync.WaitGroup // wait for all websockets to close on exit quit chan struct{} + // bgCtx is the context form of quit: it is canceled by Close and bounds + // node-local work that a handler starts and that has to outlive the + // request or connection which triggered it. + bgCtx context.Context + bgCancel context.CancelFunc + overlay *swarm.Address publicKey ecdsa.PublicKey pssPublicKey ecdsa.PublicKey @@ -342,6 +348,7 @@ func (s *Service) Configure(signer crypto.Signer, tracer *tracing.Tracer, o Opti s.metrics = newMetrics() s.quit = make(chan struct{}) + s.bgCtx, s.bgCancel = context.WithCancel(context.Background()) s.storer = e.Storer s.resolver = e.Resolver @@ -402,6 +409,7 @@ func (s *Service) SetIsWarmingUp(v bool) { func (s *Service) Close() error { s.logger.Info("api shutting down") close(s.quit) + s.bgCancel() done := make(chan struct{}) go func() { diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index c7f168e62fd..8674ac4ff3d 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -6,7 +6,6 @@ package api import ( "bytes" - "context" "fmt" "net/http" "strings" @@ -14,6 +13,7 @@ import ( "time" "github.com/ethersphere/bee/v2/pkg/jsonhttp" + "github.com/ethersphere/bee/v2/pkg/safe" "github.com/ethersphere/bee/v2/pkg/soc" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/gorilla/mux" @@ -183,12 +183,20 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { wake := make(chan struct{}, 1) cleanup := s.gsoc.Subscribe(paths.Address, func(c *soc.SOC) { if headers.CacheWrappedChunk { - // Caching is a node-local side effect independent of this - // subscriber's connection, so it must not be aborted just - // because the websocket closes mid-write. - if err := s.storer.Cache().Put(context.Background(), c.WrappedChunk()); err != nil { - s.logger.Debug("gsoc ws: cache wrapped chunk failed", "error", err) - } + // This callback runs inline on the push and pull sync stream + // goroutines, so the store write is handed to its own goroutine: + // done here it would hold up the chunk's own storage and, on the + // push sync path, the receipt the sending peer is waiting for. + // Caching is also a node-local side effect independent of this + // subscriber's connection, so it must not be aborted just because + // the websocket closes mid-write; it is bound to the node's + // lifetime instead. + wrapped := c.WrappedChunk() + safe.Go(s.logger, "gsoc-cache-wrapped-chunk", func() { + if err := s.storer.Cache().Put(s.bgCtx, wrapped); err != nil { + s.logger.Debug("gsoc ws: cache wrapped chunk failed", "error", err) + } + }) } b, err := socFieldsBytes(c, fields) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 8c325b71b97..83cbef0a0c8 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -27,6 +27,7 @@ import ( "github.com/ethersphere/bee/v2/pkg/log" mockbatchstore "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock" "github.com/ethersphere/bee/v2/pkg/soc" + "github.com/ethersphere/bee/v2/pkg/spinlock" mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/util/testutil" @@ -685,8 +686,17 @@ func TestGsocWebsocketCacheWrappedChunk(t *testing.T) { t.Fatal(err) } - got, err := storer.ChunkStore().Get(context.Background(), ch.Address()) - if err != nil { + // The chunk is cached on its own goroutine, so it may not be in the store + // yet by the time the message reaches the client. + var got swarm.Chunk + if err := spinlock.Wait(longTimeout, func() bool { + c, err := storer.ChunkStore().Get(context.Background(), ch.Address()) + if err != nil { + return false + } + got = c + return true + }); err != nil { t.Fatalf("wrapped chunk not cached: %v", err) } if !bytes.Equal(got.Data(), ch.Data()) { From f98a1b5dd94d47f889fb327cd768b73b479fad24 Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 23 Sep 2026 17:39:48 +0200 Subject: [PATCH 31/33] refactor: improve subscription handling --- pkg/api/api.go | 6 ++ pkg/api/export_test.go | 4 + pkg/api/gsoc.go | 91 +++++++++++++++++----- pkg/api/gsoc_test.go | 168 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 18 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 2c53748dbb8..c9e0d19b969 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -188,6 +188,11 @@ type Service struct { bgCtx context.Context bgCancel context.CancelFunc + // gsocCacheSubs holds the caching subscriptions shared by the GSOC + // websocket subscribers, keyed by GSOC address, see gsoc.go. + gsocCacheMu sync.Mutex + gsocCacheSubs map[string]*gsocCacheSub + overlay *swarm.Address publicKey ecdsa.PublicKey pssPublicKey ecdsa.PublicKey @@ -349,6 +354,7 @@ func (s *Service) Configure(signer crypto.Signer, tracer *tracing.Tracer, o Opti s.quit = make(chan struct{}) s.bgCtx, s.bgCancel = context.WithCancel(context.Background()) + s.gsocCacheSubs = make(map[string]*gsocCacheSub) s.storer = e.Storer s.resolver = e.Resolver diff --git a/pkg/api/export_test.go b/pkg/api/export_test.go index 9b869dd2dc8..dde50ea7b2a 100644 --- a/pkg/api/export_test.go +++ b/pkg/api/export_test.go @@ -55,6 +55,10 @@ func (q *GsocQueue) Push(b []byte) { q.push(b) } func (q *GsocQueue) Pop() ([]byte, bool) { return q.pop() } func (q *GsocQueue) Release() { q.release() } +func (s *Service) CacheGsocWrappedChunks(address swarm.Address) func() { + return s.cacheGsocWrappedChunks(address) +} + var ( FileSizeBucketsKBytes = fileSizeBucketsKBytes ToFileSizeBucket = toFileSizeBucket diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index 8674ac4ff3d..7e0504b5976 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -179,26 +179,19 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { // instead of inside the spawned goroutine closes the window in which an // update could arrive before the handler is registered and be silently // missed. + // + // Caching the wrapped chunks is a subscription of its own, shared by every + // subscriber of this address that asked for it. It is registered first, so + // that a chunk arriving in between is cached rather than announced to a + // client that cannot resolve it yet. + releaseCache := func() {} + if headers.CacheWrappedChunk { + releaseCache = s.cacheGsocWrappedChunks(paths.Address) + } + queue := newGsocQueue() wake := make(chan struct{}, 1) cleanup := s.gsoc.Subscribe(paths.Address, func(c *soc.SOC) { - if headers.CacheWrappedChunk { - // This callback runs inline on the push and pull sync stream - // goroutines, so the store write is handed to its own goroutine: - // done here it would hold up the chunk's own storage and, on the - // push sync path, the receipt the sending peer is waiting for. - // Caching is also a node-local side effect independent of this - // subscriber's connection, so it must not be aborted just because - // the websocket closes mid-write; it is bound to the node's - // lifetime instead. - wrapped := c.WrappedChunk() - safe.Go(s.logger, "gsoc-cache-wrapped-chunk", func() { - if err := s.storer.Cache().Put(s.bgCtx, wrapped); err != nil { - s.logger.Debug("gsoc ws: cache wrapped chunk failed", "error", err) - } - }) - } - b, err := socFieldsBytes(c, fields) if err != nil { s.logger.Warning("gsoc ws: serialize soc fields failed", "error", err) @@ -217,7 +210,69 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { }) s.wsWg.Add(1) - go s.gsocListeningWs(conn, cleanup, queue, wake) + go s.gsocListeningWs(conn, func() { + cleanup() + releaseCache() + }, queue, wake) +} + +// gsocCacheSub is the subscription that caches the wrapped chunks delivered on +// one GSOC address, together with the number of websocket subscribers of that +// address currently interested in it. Caching is a node-local side effect, so +// it is done once per chunk no matter how many subscribers asked for it: done +// per subscriber instead, the very same chunk would be written to the store as +// many times as there are subscribers. +type gsocCacheSub struct { + refs int + cleanup func() +} + +// cacheGsocWrappedChunks subscribes to the given GSOC address to cache the +// wrapped chunk of every message delivered on it, unless that subscription is +// already in place, and returns the release of this caller's interest in it. +// The subscription ends with the release of the last interest in it. +func (s *Service) cacheGsocWrappedChunks(address swarm.Address) (release func()) { + key := address.ByteString() + + s.gsocCacheMu.Lock() + defer s.gsocCacheMu.Unlock() + + sub, ok := s.gsocCacheSubs[key] + if !ok { + sub = &gsocCacheSub{} + sub.cleanup = s.gsoc.Subscribe(address, func(c *soc.SOC) { + // This callback runs inline on the push and pull sync stream + // goroutines, so the store write is handed to its own goroutine: + // done here it would hold up the chunk's own storage and, on the + // push sync path, the receipt the sending peer is waiting for. + // Caching is also independent of any single subscriber's + // connection, so it must not be aborted just because a websocket + // closes mid-write; it is bound to the node's lifetime instead. + wrapped := c.WrappedChunk() + safe.Go(s.logger, "gsoc-cache-wrapped-chunk", func() { + if err := s.storer.Cache().Put(s.bgCtx, wrapped); err != nil { + s.logger.Debug("gsoc ws: cache wrapped chunk failed", "error", err) + } + }) + }) + s.gsocCacheSubs[key] = sub + } + sub.refs++ + + var once sync.Once + return func() { + once.Do(func() { + s.gsocCacheMu.Lock() + defer s.gsocCacheMu.Unlock() + + sub.refs-- + if sub.refs > 0 { + return + } + delete(s.gsocCacheSubs, key) + sub.cleanup() + }) + } } // gsocQueue is a bounded FIFO ring buffer of pending outgoing GSOC messages. diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 83cbef0a0c8..bd5c9c1911f 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -15,6 +15,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "testing" "time" @@ -28,6 +29,8 @@ import ( mockbatchstore "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock" "github.com/ethersphere/bee/v2/pkg/soc" "github.com/ethersphere/bee/v2/pkg/spinlock" + "github.com/ethersphere/bee/v2/pkg/storage" + "github.com/ethersphere/bee/v2/pkg/storage/inmemchunkstore" mockstorer "github.com/ethersphere/bee/v2/pkg/storer/mock" "github.com/ethersphere/bee/v2/pkg/swarm" "github.com/ethersphere/bee/v2/pkg/util/testutil" @@ -704,6 +707,171 @@ func TestGsocWebsocketCacheWrappedChunk(t *testing.T) { } } +// TestGsocWebsocketCacheWrappedChunkOnce verifies that the wrapped chunk of a +// message is written to the cache once, no matter how many subscribers of the +// address asked for it: the store write is a node-local side effect done on +// behalf of the node, not of a connection, and repeating it per subscriber +// would multiply the work the sync stream goroutines pay for. +func TestGsocWebsocketCacheWrappedChunkOnce(t *testing.T) { + t.Parallel() + + var ( + id = make([]byte, 32) + headers = http.Header{api.SwarmCacheWrappedChunkHeader: []string{"true"}} + chunkStore = &countingChunkStore{ChunkStore: inmemchunkstore.New()} + gsocSvc = &countingListener{Listener: gsoc.New(log.Noop)} + payloads = [][]byte{[]byte("the first message"), []byte("the second message")} + ) + testutil.CleanupCloser(t, gsocSvc) + + privKey, err := crypto.GenerateSecp256k1Key() + if err != nil { + t.Fatal(err) + } + signer := crypto.NewDefaultSigner(privKey) + owner, err := signer.EthereumAddress() + if err != nil { + t.Fatal(err) + } + chunkAddr, _ := soc.CreateAddress(id, owner.Bytes()) + wsPath := "/gsoc/subscribe/" + hex.EncodeToString(chunkAddr.Bytes()) + + _, cl, listener, _, _ := newTestServer(t, testServerOptions{ + Gsoc: gsocSvc, + WsPath: wsPath, + WsHeaders: headers, + Storer: mockstorer.NewWithChunkStore(chunkStore), + BatchStore: mockbatchstore.New(), + Logger: log.Noop, + WsPingPeriod: 10 * time.Second, + }) + + u := url.URL{Scheme: "ws", Host: listener, Path: wsPath} + cl2, _, err := websocket.DefaultDialer.Dial(u.String(), headers) + if err != nil { + t.Fatalf("dial: %v. url %v", err, u.String()) + } + testutil.CleanupCloser(t, cl2) + + for _, c := range []*websocket.Conn{cl, cl2} { + if err := c.SetReadDeadline(time.Now().Add(longTimeout)); err != nil { + t.Fatal(err) + } + c.SetReadLimit(swarm.ChunkSize) + } + + // Neither Dial returning nor the websocket handshake says that the + // subscription behind the connection is registered, so wait for the + // subscriptions themselves: one per connection, plus the single caching + // one the two of them share. + if err := spinlock.Wait(longTimeout, func() bool { return gsocSvc.subs.Load() >= 3 }); err != nil { + t.Fatalf("subscriptions not registered: %v", err) + } + + for _, payload := range payloads { + ch, _ := cac.New(payload) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + gsocSvc.Handle(socCh) + + respC := make(chan error, 2) + go expectMessage(t, cl, respC, payload) + go expectMessage(t, cl2, respC, payload) + for range 2 { + if err := <-respC; err != nil { + t.Fatal(err) + } + } + + // The chunk is cached on its own goroutine, so it may not be in the + // store yet by the time the messages reach the clients. + if err := spinlock.Wait(longTimeout, func() bool { + _, err := chunkStore.Get(context.Background(), ch.Address()) + return err == nil + }); err != nil { + t.Fatalf("wrapped chunk not cached: %v", err) + } + } + + // Both messages are cached by now, so a write per subscriber rather than + // per message would already have happened. + if puts, want := chunkStore.puts.Load(), int32(len(payloads)); puts != want { + t.Fatalf("got %d cache writes, want %d: one per message, not one per subscriber", puts, want) + } +} + +// TestGsocCacheSubscriptionShared verifies the bookkeeping behind that single +// cache write: the subscribers of one address share a single caching +// subscription, which is registered with the first of them and dropped only +// once the last one is gone. +func TestGsocCacheSubscriptionShared(t *testing.T) { + t.Parallel() + + var ( + address = swarm.RandAddress(t) + gsocSvc = &countingListener{Listener: gsoc.New(log.Noop)} + ) + testutil.CleanupCloser(t, gsocSvc) + + _, _, _, _, svc := newTestServer(t, testServerOptions{ + Gsoc: gsocSvc, + Storer: mockstorer.New(), + BatchStore: mockbatchstore.New(), + Logger: log.Noop, + }) + + first := svc.CacheGsocWrappedChunks(address) + second := svc.CacheGsocWrappedChunks(address) + if subs := gsocSvc.subs.Load(); subs != 1 { + t.Fatalf("got %d subscriptions for two subscribers, want 1", subs) + } + + first() + if subs := gsocSvc.subs.Load(); subs != 1 { + t.Fatalf("got %d subscriptions after one subscriber left, want the other one to keep it", subs) + } + + second() + if subs := gsocSvc.subs.Load(); subs != 0 { + t.Fatalf("got %d subscriptions after the last subscriber left, want 0", subs) + } + + // A subscriber arriving after the address was dropped subscribes anew. + third := svc.CacheGsocWrappedChunks(address) + if subs := gsocSvc.subs.Load(); subs != 1 { + t.Fatalf("got %d subscriptions for a new subscriber, want 1", subs) + } + third() +} + +// countingChunkStore counts the chunks written to the store it wraps. +type countingChunkStore struct { + storage.ChunkStore + puts atomic.Int32 +} + +func (s *countingChunkStore) Put(ctx context.Context, ch swarm.Chunk) error { + s.puts.Add(1) + return s.ChunkStore.Put(ctx, ch) +} + +// countingListener counts the subscriptions registered on the GSOC listener it +// wraps, so that a test can wait for the api to have registered all of them. +type countingListener struct { + gsoc.Listener + subs atomic.Int32 +} + +func (l *countingListener) Subscribe(address swarm.Address, handler gsoc.Handler) func() { + cleanup := l.Listener.Subscribe(address, handler) + l.subs.Add(1) + return func() { + cleanup() + l.subs.Add(-1) + } +} + func newGsocTest(t *testing.T, socId []byte, pingPeriod time.Duration) (gsoc.Listener, *websocket.Conn, crypto.Signer, string) { t.Helper() g, cl, signer, listener, _ := newGsocTestWithOpts(t, socId, pingPeriod, nil) From a6fa28f014e1def8ae0dbba6814768b526225d28 Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 23 Sep 2026 18:10:53 +0200 Subject: [PATCH 32/33] refactor: add query parameter support --- openapi/Swarm.yaml | 5 ++- openapi/SwarmCommon.yaml | 22 +++++++++++ pkg/api/api_test.go | 3 +- pkg/api/gsoc.go | 28 +++++++++++--- pkg/api/gsoc_test.go | 80 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 130 insertions(+), 8 deletions(-) diff --git a/openapi/Swarm.yaml b/openapi/Swarm.yaml index 42e97e094c9..cdabd8635cc 100644 --- a/openapi/Swarm.yaml +++ b/openapi/Swarm.yaml @@ -969,13 +969,16 @@ paths: description: "Single Owner Chunk address (which may have multiple payloads)" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmSocFieldsParameter" - $ref: "SwarmCommon.yaml#/components/parameters/SwarmCacheWrappedChunkParameter" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmSocFieldsQueryParameter" + - $ref: "SwarmCommon.yaml#/components/parameters/SwarmCacheWrappedChunkQueryParameter" responses: "200": description: > Establishes a WebSocket subscription for incoming messages on the Single Owner Chunk address. Each message is the binary serialization of the Single Owner Chunk fields requested through the - swarm-soc-fields header (defaults to the wrapped chunk payload). + swarm-soc-fields header or query parameter (defaults to the wrapped + chunk payload). Pending messages are buffered per subscription up to a fixed limit; a client that does not keep up with the incoming rate misses the oldest of them, as they are discarded in favor of the newest ones. diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index d53eca54c92..39f08775ac6 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -1206,6 +1206,28 @@ components: be cached locally so that it can be resolved through the bytes endpoint (useful when the single owner chunk wraps a root chunk larger than 4KB). + SwarmSocFieldsQueryParameter: + in: query + name: swarm-soc-fields + schema: + type: string + required: false + description: > + Same as the swarm-soc-fields header, for clients that cannot set request + headers (e.g. browser WebSocket). Takes precedence over the header when + both are given. + + SwarmCacheWrappedChunkQueryParameter: + in: query + name: swarm-cache-wrapped-chunk + schema: + type: boolean + required: false + description: > + Same as the swarm-cache-wrapped-chunk header, for clients that cannot set + request headers (e.g. browser WebSocket). Takes precedence over the header + when both are given. + SwarmPinParameter: in: header name: swarm-pin diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 8b0fc2eebf3..02de8ce6d68 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -108,6 +108,7 @@ type testServerOptions struct { AccessControl accesscontrol.Controller Steward steward.Interface WsHeaders http.Header + WsQuery url.Values DirectUpload bool Probe *api.Probe @@ -295,7 +296,7 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket. ) if o.WsPath != "" { - u := url.URL{Scheme: "ws", Host: ts.Listener.Addr().String(), Path: o.WsPath} + u := url.URL{Scheme: "ws", Host: ts.Listener.Addr().String(), Path: o.WsPath, RawQuery: o.WsQuery.Encode()} conn, _, err = websocket.DefaultDialer.Dial(u.String(), o.WsHeaders) if err != nil { t.Fatalf("dial: %v. url %v", err, u.String()) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index 7e0504b5976..c0c54ab695c 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -139,18 +139,36 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { headers := struct { SocFields string `map:"Swarm-Soc-Fields"` - CacheWrappedChunk bool `map:"Swarm-Cache-Wrapped-Chunk"` + CacheWrappedChunk *bool `map:"Swarm-Cache-Wrapped-Chunk"` }{} if response := s.mapStructure(r.Header, &headers); response != nil { response("invalid header params", logger, w) return } + // Browser WebSocket clients cannot set request headers, so the same + // options are also accepted as query parameters. Query parameters take + // precedence over headers. + queries := struct { + SocFields string `map:"swarm-soc-fields"` + CacheWrappedChunk *bool `map:"swarm-cache-wrapped-chunk"` + }{} + if response := s.mapStructure(r.URL.Query(), &queries); response != nil { + response("invalid query params", logger, w) + return + } + if queries.SocFields != "" { + headers.SocFields = queries.SocFields + } + if queries.CacheWrappedChunk != nil { + headers.CacheWrappedChunk = queries.CacheWrappedChunk + } + fields, err := parseSocFields(headers.SocFields) if err != nil { - logger.Debug("invalid soc fields header", "error", err) - logger.Error(nil, "invalid soc fields header") - jsonhttp.BadRequest(w, "invalid soc fields header") + logger.Debug("invalid soc fields", "error", err) + logger.Error(nil, "invalid soc fields") + jsonhttp.BadRequest(w, "invalid soc fields") return } @@ -185,7 +203,7 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { // that a chunk arriving in between is cached rather than announced to a // client that cannot resolve it yet. releaseCache := func() {} - if headers.CacheWrappedChunk { + if headers.CacheWrappedChunk != nil && *headers.CacheWrappedChunk { releaseCache = s.cacheGsocWrappedChunks(paths.Address) } diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index bd5c9c1911f..336c28fde3c 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -301,12 +301,84 @@ func TestGsocWebsocketInvalidFieldsHeader(t *testing.T) { jsonhttptest.Request(t, client, http.MethodGet, "/gsoc/subscribe/"+addrHex, http.StatusBadRequest, jsonhttptest.WithRequestHeader(api.SwarmSocFieldsHeader, "bogusfield"), jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{ - Message: "invalid soc fields header", + Message: "invalid soc fields", Code: http.StatusBadRequest, }), ) } +// TestGsocWebsocketQueryParams verifies that the subscription options can also +// be given as query parameters, since browser WebSocket clients cannot set +// request headers, and that query parameters take precedence over headers. +func TestGsocWebsocketQueryParams(t *testing.T) { + t.Parallel() + + payload := []byte("Any sufficiently advanced technology is indistinguishable from magic.") + + for _, tc := range []struct { + name string + headers http.Header + query url.Values + fields func(id []byte, ch swarm.Chunk) []byte + cached bool + }{ + { + name: "query only", + query: url.Values{"swarm-soc-fields": {"identifier,payload"}, "swarm-cache-wrapped-chunk": {"true"}}, + fields: func(id []byte, ch swarm.Chunk) []byte { return slices.Concat(id, payload) }, + cached: true, + }, + { + name: "query precedence", + headers: http.Header{ + api.SwarmSocFieldsHeader: []string{"identifier,payload"}, + api.SwarmCacheWrappedChunkHeader: []string{"true"}, + }, + query: url.Values{"swarm-soc-fields": {"span,payload"}, "swarm-cache-wrapped-chunk": {"false"}}, + fields: func(id []byte, ch swarm.Chunk) []byte { return ch.Data() }, + cached: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var ( + id = make([]byte, 32) + g, cl, signer, _, storer = newGsocTestWithQuery(t, id, 0, tc.headers, tc.query) + respC = make(chan error, 1) + ) + + err := cl.SetReadDeadline(time.Now().Add(longTimeout)) + if err != nil { + t.Fatal(err) + } + cl.SetReadLimit(swarm.ChunkSize) + + ch, _ := cac.New(payload) + socCh := soc.New(id, ch) + signedCh, _ := socCh.Sign(signer) + socCh, _ = soc.FromChunk(signedCh) + g.Handle(socCh) + + go expectMessage(t, cl, respC, tc.fields(id, ch)) + if err := <-respC; err != nil { + t.Fatal(err) + } + + if tc.cached { + if err := spinlock.Wait(longTimeout, func() bool { + _, err := storer.ChunkStore().Get(context.Background(), ch.Address()) + return err == nil + }); err != nil { + t.Fatalf("wrapped chunk not cached: %v", err) + } + } else if _, err := storer.ChunkStore().Get(context.Background(), ch.Address()); err == nil { + t.Fatal("wrapped chunk cached despite query precedence") + } + }) + } +} + // TestGsocWebsocketSlowConsumer verifies that a subscriber that falls behind // incoming GSOC messages is not dropped or disconnected: the server queues the // messages and delivers the backlog, in order, once the consumer catches up, @@ -879,6 +951,11 @@ func newGsocTest(t *testing.T, socId []byte, pingPeriod time.Duration) (gsoc.Lis } func newGsocTestWithOpts(t *testing.T, socId []byte, pingPeriod time.Duration, headers http.Header) (gsoc.Listener, *websocket.Conn, crypto.Signer, string, api.Storer) { + t.Helper() + return newGsocTestWithQuery(t, socId, pingPeriod, headers, nil) +} + +func newGsocTestWithQuery(t *testing.T, socId []byte, pingPeriod time.Duration, headers http.Header, query url.Values) (gsoc.Listener, *websocket.Conn, crypto.Signer, string, api.Storer) { t.Helper() if pingPeriod == 0 { pingPeriod = 10 * time.Second @@ -906,6 +983,7 @@ func newGsocTestWithOpts(t *testing.T, socId []byte, pingPeriod time.Duration, h Gsoc: gsoc, WsPath: fmt.Sprintf("/gsoc/subscribe/%s", hex.EncodeToString(chunkAddr.Bytes())), WsHeaders: headers, + WsQuery: query, Storer: storer, BatchStore: batchStore, Logger: log.Noop, From 933b324446596a226b646969e1455906e429857e Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 23 Sep 2026 18:20:28 +0200 Subject: [PATCH 33/33] refactor: improve websocket message handling --- pkg/api/gsoc.go | 67 +++++++++++++++++-------------------- pkg/api/gsoc_test.go | 79 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 101 insertions(+), 45 deletions(-) diff --git a/pkg/api/gsoc.go b/pkg/api/gsoc.go index c0c54ab695c..55361938e22 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -350,8 +350,8 @@ func (q *gsocQueue) pop() (b []byte, ok bool) { } // droppedCount returns how many messages were evicted since the previous call -// and resets the counter, so that a slow subscriber is reported once per drain -// instead of once per lost message. +// and resets the counter, so that a slow subscriber is reported once per +// reporting period instead of once per lost message. func (q *gsocQueue) droppedCount() uint64 { q.mu.Lock() defer q.mu.Unlock() @@ -403,44 +403,30 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, cleanup func(), queue *g for { select { case <-wake: - for { - // Draining a backlog must not outlast the node. A consumer - // that keeps every write just under the write deadline makes - // each message cost seconds, so a full queue would otherwise - // hold this goroutine for minutes: long past the second that - // Close waits for it, leaving the shutdown to report open - // websockets and starving the keepalive ping in the meantime. - // Re-check the exits between messages to bound that to the - // single write already in flight. - select { - case <-s.quit: - s.gsocWsNotifyClose(conn) - return - case <-gone: - return - default: - } - - b, ok := queue.pop() - if !ok { - break - } - - err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) - if err != nil { - s.logger.Debug("gsoc ws: set write deadline failed", "error", err) - return - } + // Write a single message per iteration and re-arm wake, instead + // of draining the whole queue here: the outer select then keeps + // serving the keepalive ping, the shutdown and the client-gone + // signals between messages, however long the backlog stays + // non-empty. A spurious wake just finds the queue empty. + b, ok := queue.pop() + if !ok { + continue + } + select { + case wake <- struct{}{}: + default: + } - err = conn.WriteMessage(websocket.BinaryMessage, b) - if err != nil { - s.logger.Debug("gsoc ws: write message failed", "error", err) - return - } + err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) + if err != nil { + s.logger.Debug("gsoc ws: set write deadline failed", "error", err) + return } - if dropped := queue.droppedCount(); dropped > 0 { - s.logger.Warning("gsoc ws: subscriber too slow, messages dropped", "count", dropped) + err = conn.WriteMessage(websocket.BinaryMessage, b) + if err != nil { + s.logger.Debug("gsoc ws: write message failed", "error", err) + return } case <-s.quit: @@ -451,6 +437,13 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, cleanup func(), queue *g // client gone return case <-ticker.C: + // Report evictions once per ping period rather than per message, + // so that a subscriber that stays behind is reported periodically + // without flooding the operator log. + if dropped := queue.droppedCount(); dropped > 0 { + s.logger.Warning("gsoc ws: subscriber too slow, messages dropped", "count", dropped) + } + err = conn.SetWriteDeadline(time.Now().Add(writeDeadline)) if err != nil { s.logger.Debug("gsoc ws: set write deadline failed", "error", err) diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index 336c28fde3c..3bbcd566ca5 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -390,7 +390,7 @@ func TestGsocWebsocketSlowConsumer(t *testing.T) { const messageCount = 10 id := make([]byte, 32) - gsocSvc, cl, signer := newGsocPipeTest(t, id) + gsocSvc, cl, signer := newGsocPipeTest(t, id, 0) // Build the chunks before publishing any of them: from the first publish // until the consumer starts reading again the server's writer is blocked @@ -436,7 +436,7 @@ func TestGsocWebsocketQueueBound(t *testing.T) { const messageCount = api.GsocQueueCapacity + 16 id := make([]byte, 32) - gsocSvc, cl, signer := newGsocPipeTest(t, id) + gsocSvc, cl, signer := newGsocPipeTest(t, id, 0) // Build every chunk before publishing any of them. Signing and recovering // a few hundred single owner chunks is the expensive part of this test, @@ -493,6 +493,68 @@ func TestGsocWebsocketQueueBound(t *testing.T) { } } +// TestGsocWebsocketPingUnderBacklog verifies that the keepalive ping keeps +// flowing while the subscriber lags behind a continuous stream of messages, so +// that a backlog that never drains cannot starve the ping and make the client +// drop the connection on its own pong timeout. The connection runs over an +// unbuffered pipe, so every write blocks until the client reads it and the +// queue stays non-empty for the whole test. +func TestGsocWebsocketPingUnderBacklog(t *testing.T) { + t.Parallel() + + const ( + pingPeriod = 50 * time.Millisecond + duration = time.Second + readDelay = 5 * time.Millisecond + minPings = 5 + ) + + var ( + id = make([]byte, 32) + gsocSvc, cl, signer = newGsocPipeTest(t, id, pingPeriod) + pings atomic.Int64 + ) + + ch, _ := cac.New([]byte("keep the writer busy")) + signedCh, _ := soc.New(id, ch).Sign(signer) + socCh, _ := soc.FromChunk(signedCh) + + cl.SetPingHandler(func(string) error { + pings.Add(1) + return nil + }) + + stop := make(chan struct{}) + done := make(chan struct{}) + go func() { + defer close(done) + for { + select { + case <-stop: + return + default: + gsocSvc.Handle(socCh) + time.Sleep(time.Millisecond) + } + } + }() + defer func() { + close(stop) + <-done + }() + + for end := time.Now().Add(duration); time.Now().Before(end); { + if _, _, err := cl.ReadMessage(); err != nil { + t.Fatalf("read: %v", err) + } + time.Sleep(readDelay) + } + + if got := pings.Load(); got < minPings { + t.Fatalf("got %d pings in %v at a %v period, want at least %d", got, duration, pingPeriod, minPings) + } +} + // TestGsocWebsocketStalledConsumer verifies the failure mode of a subscriber // that never reads: the writer cannot hand its message over, so the write // deadline fires and it gives up on the connection. On its way out it closes @@ -504,7 +566,7 @@ func TestGsocWebsocketStalledConsumer(t *testing.T) { t.Parallel() id := make([]byte, 32) - gsocSvc, cl, signer := newGsocPipeTest(t, id) + gsocSvc, cl, signer := newGsocPipeTest(t, id, 0) // a single message suffices: the pipe is unbuffered, so this one write // pins the writer until its deadline expires. @@ -554,7 +616,7 @@ func TestGsocQueueRelease(t *testing.T) { // backlog in its kernel buffers instead. It returns once the subscription is // registered, handing back the listener to publish through, the client end of // the subscription and the signer owning the subscribed address. -func newGsocPipeTest(t *testing.T, socID []byte) (*subscribedListener, *websocket.Conn, crypto.Signer) { +func newGsocPipeTest(t *testing.T, socID []byte, pingPeriod time.Duration) (*subscribedListener, *websocket.Conn, crypto.Signer) { t.Helper() var ( @@ -565,10 +627,11 @@ func newGsocPipeTest(t *testing.T, socID []byte) (*subscribedListener, *websocke testutil.CleanupCloser(t, gsocSvc) _, _, _, _, svc := newTestServer(t, testServerOptions{ - Gsoc: gsocSvc, - Storer: storer, - BatchStore: batchStore, - Logger: log.Noop, + Gsoc: gsocSvc, + Storer: storer, + BatchStore: batchStore, + Logger: log.Noop, + WsPingPeriod: pingPeriod, }) privKey, err := crypto.GenerateSecp256k1Key()