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/openapi/Swarm.yaml b/openapi/Swarm.yaml index c1c13f2c784..cdabd8635cc 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" @@ -967,9 +967,23 @@ paths: $ref: "SwarmCommon.yaml#/components/schemas/SwarmAddress" required: true description: "Single Owner Chunk address (which may have multiple payloads)" - responses: - "200": - description: Establishes a WebSocket subscription for incoming messages on the Single Owner Chunk address + - $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 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. + "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 0634846b2a5..39f08775ac6 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -1180,6 +1180,54 @@ components: required: false description: Associate upload with an existing Tag UID + SwarmSocFieldsParameter: + in: header + name: swarm-soc-fields + schema: + type: string + default: "payload" + 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". + 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 + 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). + + 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/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.go b/pkg/api/api.go index a6b74837ef4..c9e0d19b969 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" @@ -180,6 +182,17 @@ 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 + + // 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 @@ -340,6 +353,8 @@ 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.gsocCacheSubs = make(map[string]*gsocCacheSub) s.storer = e.Storer s.resolver = e.Resolver @@ -400,6 +415,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() { @@ -607,6 +623,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/api_test.go b/pkg/api/api_test.go index e6be53ea4e4..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 @@ -141,7 +142,12 @@ type testServerOptions struct { Erc20ServiceNil bool } -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) @@ -290,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()) @@ -304,7 +310,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 { @@ -454,7 +460,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, @@ -469,7 +475,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, @@ -485,7 +491,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, @@ -505,7 +511,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 @@ -556,7 +562,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, @@ -592,7 +598,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, @@ -798,7 +804,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, 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/export_test.go b/pkg/api/export_test.go index 0eb164a8cbe..dde50ea7b2a 100644 --- a/pkg/api/export_test.go +++ b/pkg/api/export_test.go @@ -45,6 +45,20 @@ var ( SuccessWsMsg = successWsMsg ) +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() } + +func (s *Service) CacheGsocWrappedChunks(address swarm.Address) func() { + return s.cacheGsocWrappedChunks(address) +} + var ( FileSizeBucketsKBytes = fileSizeBucketsKBytes ToFileSizeBucket = toFileSizeBucket 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.go b/pkg/api/gsoc.go index 60d048ffdc0..55361938e22 100644 --- a/pkg/api/gsoc.go +++ b/pkg/api/gsoc.go @@ -5,15 +5,126 @@ package api import ( + "bytes" + "fmt" "net/http" + "strings" + "sync" "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" "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" +) + +// 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 + +// 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. 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(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(socFieldSizes)) + parts := strings.Split(header, ",") + fields := make([]string, 0, len(socFieldSizes)) + for _, p := range parts { + f := strings.ToLower(strings.TrimSpace(p)) + if f == "" { + continue + } + if _, ok := socFieldSizes[f]; !ok { + return nil, fmt.Errorf("unknown soc field: %q", p) + } + if seen[f] { + continue + } + seen[f] = true + 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 +137,49 @@ 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 + } + + // 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", "error", err) + logger.Error(nil, "invalid soc fields") + jsonhttp.BadRequest(w, "invalid soc fields") + 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, } @@ -40,15 +191,200 @@ func (s *Service) gsocWsHandler(w http.ResponseWriter, r *http.Request) { return } + // 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. + // + // 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 != nil && *headers.CacheWrappedChunk { + releaseCache = s.cacheGsocWrappedChunks(paths.Address) + } + + queue := newGsocQueue() + wake := make(chan struct{}, 1) + cleanup := s.gsoc.Subscribe(paths.Address, func(c *soc.SOC) { + b, err := socFieldsBytes(c, fields) + if err != nil { + s.logger.Warning("gsoc ws: serialize soc fields failed", "error", err) + return + } + + 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 + // means it is already going to pick this up. + select { + case wake <- struct{}{}: + default: + } + }) + s.wsWg.Add(1) - go s.gsocListeningWs(conn, paths.Address) + go s.gsocListeningWs(conn, func() { + cleanup() + releaseCache() + }, queue, wake) } -func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address) { +// 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. +// +// 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 // 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. 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) + 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 q.size == 0 { + return nil, false + } + 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 +// reporting period 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 +} + +// 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 ( - dataC = make(chan []byte) gone = make(chan struct{}) ticker = time.NewTicker(s.WsPingPeriod) err error @@ -57,17 +393,6 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address ticker.Stop() _ = conn.Close() }() - cleanup := s.gsoc.Subscribe(socAddress, func(m []byte) { - select { - case dataC <- m: - case <-gone: - return - case <-s.quit: - return - } - }) - - defer cleanup() conn.SetCloseHandler(func(code int, text string) error { s.logger.Debug("gsoc ws: client gone", "code", code, "message", text) @@ -77,7 +402,21 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address for { select { - case b := <-dataC: + case <-wake: + // 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.SetWriteDeadline(time.Now().Add(writeDeadline)) if err != nil { s.logger.Debug("gsoc ws: set write deadline failed", "error", err) @@ -92,20 +431,19 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address 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 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) @@ -118,3 +456,15 @@ func (s *Service) gsocListeningWs(conn *websocket.Conn, socAddress swarm.Address } } } + +// 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) + } +} diff --git a/pkg/api/gsoc_test.go b/pkg/api/gsoc_test.go index cf3161a9f03..3bbcd566ca5 100644 --- a/pkg/api/gsoc_test.go +++ b/pkg/api/gsoc_test.go @@ -5,19 +5,32 @@ package api_test import ( + "bytes" + "context" "encoding/hex" "fmt" + "net" + "net/http" "net/url" + "slices" "strings" + "sync" + "sync/atomic" "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" + "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" + "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" @@ -134,7 +147,878 @@ 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 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{"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.") + ) + + 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) + + 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 { + t.Fatal(err) + } +} + +// 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) + } +} + +// 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", + 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, +// instead of racing on the underlying websocket connection or blocking the +// (synchronous) GSOC handler indefinitely. +func TestGsocWebsocketSlowConsumer(t *testing.T) { + t.Parallel() + + const messageCount = 10 + + id := make([]byte, 32) + 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 + // 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. + for _, socCh := range socs { + 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, 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, + // 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 _, socCh := range socs { + 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) + } +} + +// 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 +// 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, 0) + + // 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. +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: 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, pingPeriod time.Duration) (*subscribedListener, *websocket.Conn, crypto.Signer) { + t.Helper() + + var ( + batchStore = mockbatchstore.New() + storer = mockstorer.New() + gsocSvc = newSubscribedListener(gsoc.New(log.Noop)) + ) + testutil.CleanupCloser(t, gsocSvc) + + _, _, _, _, svc := newTestServer(t, testServerOptions{ + Gsoc: gsocSvc, + Storer: storer, + BatchStore: batchStore, + Logger: log.Noop, + WsPingPeriod: pingPeriod, + }) + + 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(socID, 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) + + // 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) + } + + return gsocSvc, cl, signer +} + +// 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{} + unsubscribed chan struct{} + subOnce sync.Once + unsubOnce sync.Once +} + +func newSubscribedListener(l gsoc.Listener) *subscribedListener { + 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.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 +// 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. +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) + } + + // 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()) { + t.Fatal("cached wrapped chunk data mismatch") + } +} + +// 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) + 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() + 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 @@ -158,14 +1042,16 @@ func newGsocTest(t *testing.T, socId []byte, pingPeriod time.Duration) (gsoc.Lis 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, + WsQuery: query, Storer: storer, BatchStore: batchStore, Logger: log.Noop, WsPingPeriod: pingPeriod, }) - return gsoc, cl, signer, listener + return gsoc, cl, signer, listener, storer } 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 })), diff --git a/pkg/gsoc/gsoc.go b/pkg/gsoc/gsoc.go index 41e4f54ac2c..bf9bd09e013 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" @@ -13,8 +14,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()) @@ -39,20 +41,35 @@ 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:]...) + 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 } } @@ -66,18 +83,19 @@ 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()) for _, hh := range h { - go func(hh Handler) { - hh(c.WrappedChunk().Data()[swarm.SpanSize:]) - }(*hh) + (*hh)(c) } } +// 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 dc49b0809a8..8ad2a8df410 100644 --- a/pkg/gsoc/gsoc_test.go +++ b/pkg/gsoc/gsoc_test.go @@ -5,8 +5,8 @@ package gsoc_test import ( + "sync" "testing" - "time" "github.com/ethersphere/bee/v2/pkg/cac" "github.com/ethersphere/bee/v2/pkg/crypto" @@ -25,7 +25,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 +36,9 @@ func TestRegister(t *testing.T) { address1, _ = soc.CreateAddress(socId1, owner.Bytes()) address2, _ = soc.CreateAddress(socId2, owner.Bytes()) - h1 = func(m []byte) { - h1Calls++ - msgChan <- struct{}{} - } - - h2 = func(m []byte) { - h2Calls++ - msgChan <- struct{}{} - } - - h3 = func(m []byte) { - 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 +56,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 +64,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 +72,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) @@ -111,14 +91,55 @@ func ensureCalls(t *testing.T, calls *int, exp int) { } } -func waitHandlerCallback(t *testing.T, msgChan *chan struct{}, count int) { - t.Helper() +// 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) {} + ) - for range count { - select { - case <-*msgChan: - case <-time.After(1 * time.Second): - t.Fatal("reached timeout while waiting for handler message") - } + 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() } 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