Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 6 additions & 31 deletions openapi/Swarm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -163,37 +163,12 @@ paths:
tags:
- Bytes
parameters:
- in: header
schema:
$ref: "SwarmCommon.yaml#/components/parameters/SwarmPostageBatchId"
name: swarm-postage-batch-id
required: true
- in: header
schema:
$ref: "SwarmCommon.yaml#/components/parameters/SwarmTagParameter"
name: swarm-tag
required: false
- in: header
schema:
$ref: "SwarmCommon.yaml#/components/parameters/SwarmPinParameter"
name: swarm-pin
required: false
- in: header
schema:
$ref: "SwarmCommon.yaml#/components/parameters/SwarmDeferredUpload"
name: swarm-deferred-upload
required: false
- in: header
schema:
$ref: "SwarmCommon.yaml#/components/parameters/SwarmEncryptParameter"
name: swarm-encrypt
required: false
- in: header
schema:
$ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyLevelParameter"
name: swarm-redundancy-level
required: false

- $ref: "SwarmCommon.yaml#/components/parameters/SwarmPostageBatchId"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmTagParameter"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmPinParameter"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmDeferredUpload"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmEncryptParameter"
- $ref: "SwarmCommon.yaml#/components/parameters/SwarmRedundancyLevelParameter"
requestBody:
content:
application/octet-stream:
Expand Down
14 changes: 11 additions & 3 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,9 @@ type Service struct {

whitelistedWithdrawalAddress []common.Address

preMapHooks map[string]func(v string) (string, error)
validate *validator.Validate
preMapHooks map[string]func(v string) (string, error)
customValidationMessages map[string]func(err validator.FieldError) error
validate *validator.Validate

redistributionAgent *storageincentives.Agent

Expand Down Expand Up @@ -321,6 +322,7 @@ func New(
}
return name
})
s.setupValidation()
s.stamperStore = stamperStore

for _, v := range whitelistedWithdrawalAddress {
Expand Down Expand Up @@ -709,11 +711,17 @@ func (s *Service) mapStructure(input, output any) func(string, log.Logger, http.
case []byte:
val = string(v)
}
var cause error
if msgFn, ok := s.customValidationMessages[err.Tag()]; ok {
cause = msgFn(err)
} else {
cause = fmt.Errorf("want %s:%s", err.Tag(), err.Param())
}
vErrs = multierror.Append(vErrs,
&validationError{
Entry: strings.ToLower(err.Field()),
Value: val,
Cause: fmt.Errorf("want %s:%s", err.Tag(), err.Param()),
Cause: cause,
})
}
return response(vErrs.ErrorOrNil())
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (s *Service) bytesUploadHandler(w http.ResponseWriter, r *http.Request) {
Pin bool `map:"Swarm-Pin"`
Deferred *bool `map:"Swarm-Deferred-Upload"`
Encrypt bool `map:"Swarm-Encrypt"`
RLevel redundancy.Level `map:"Swarm-Redundancy-Level"`
RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"rLevel"`
Act bool `map:"Swarm-Act"`
HistoryAddress swarm.Address `map:"Swarm-Act-History-Address"`
}{}
Expand Down
67 changes: 67 additions & 0 deletions pkg/api/bytes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"strconv"
"testing"

"github.com/ethersphere/bee/v2/pkg/api"
"github.com/ethersphere/bee/v2/pkg/file/redundancy"
"github.com/ethersphere/bee/v2/pkg/jsonhttp"
"github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest"
"github.com/ethersphere/bee/v2/pkg/log"
Expand Down Expand Up @@ -408,3 +410,68 @@ func TestBytesDirectUpload(t *testing.T) {
}),
)
}

func TestBytesRedundancyLevel(t *testing.T) {
t.Parallel()

client, _, _, _ := newTestServer(t, testServerOptions{
Storer: mockstorer.New(),
Post: mockpost.New(mockpost.WithAcceptAll()),
})

const maxValidLevel = redundancy.PARANOID

tests := []struct {
name string
level int
want *jsonhttp.StatusResponse
}{
{"minimum level (NONE) is valid", int(redundancy.NONE), nil},
{"maximum valid level (PARANOID) is valid", int(maxValidLevel), nil},
{
"level below minimum is invalid", int(-1),
&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid header params",
Reasons: []jsonhttp.Reason{
{
Field: "Swarm-Redundancy-Level",
Error: "invalid syntax",
},
},
},
},
{
"level above maximum is invalid", int(maxValidLevel + 1),
&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid header params",
Reasons: []jsonhttp.Reason{
{
Field: "swarm-redundancy-level",
Error: fmt.Sprintf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)),
},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := []jsonhttptest.Option{
jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"),
jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, strconv.Itoa(tt.level)),
jsonhttptest.WithRequestBody(bytes.NewReader([]byte("test"))),
}
var statusCode int
if tt.want == nil {
statusCode = http.StatusCreated
} else {
statusCode = tt.want.Code
opts = append(opts, jsonhttptest.WithExpectedJSONResponse(*tt.want))
}
jsonhttptest.Request(t, client, http.MethodPost, "/bytes", statusCode, opts...)
})
}
}
6 changes: 3 additions & 3 deletions pkg/api/bzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (s *Service) bzzUploadHandler(w http.ResponseWriter, r *http.Request) {
Deferred *bool `map:"Swarm-Deferred-Upload"`
Encrypt bool `map:"Swarm-Encrypt"`
IsDir bool `map:"Swarm-Collection"`
RLevel redundancy.Level `map:"Swarm-Redundancy-Level"`
RLevel redundancy.Level `map:"Swarm-Redundancy-Level" validate:"rLevel"`
Act bool `map:"Swarm-Act"`
HistoryAddress swarm.Address `map:"Swarm-Act-History-Address"`
}{}
Expand Down Expand Up @@ -387,7 +387,7 @@ func (s *Service) serveReference(logger log.Logger, address swarm.Address, pathV
Cache *bool `map:"Swarm-Cache"`
Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"`
FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"`
RLevel *redundancy.Level `map:"Swarm-Redundancy-Level"`
RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,rLevel"`
ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"`
}{}

Expand Down Expand Up @@ -599,7 +599,7 @@ func (s *Service) serveManifestEntry(
func (s *Service) downloadHandler(logger log.Logger, w http.ResponseWriter, r *http.Request, reference swarm.Address, additionalHeaders http.Header, etag, headersOnly bool, rootCh swarm.Chunk) {
headers := struct {
Strategy *getter.Strategy `map:"Swarm-Redundancy-Strategy"`
RLevel *redundancy.Level `map:"Swarm-Redundancy-Level"`
RLevel *redundancy.Level `map:"Swarm-Redundancy-Level" validate:"omitempty,rLevel"`
FallbackMode *bool `map:"Swarm-Redundancy-Fallback-Mode"`
ChunkRetrievalTimeout *string `map:"Swarm-Chunk-Retrieval-Timeout"`
LookaheadBufferSize *int `map:"Swarm-Lookahead-Buffer-Size"`
Expand Down
141 changes: 141 additions & 0 deletions pkg/api/bzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1185,3 +1185,144 @@ func TestBzzDownloadHeaders(t *testing.T) {
jsonhttptest.WithExpectedResponseHeader(api.ContentTypeHeader, "text/html; charset=utf-8"),
)
}

func TestBzzUploadRedundancyLevel(t *testing.T) {
t.Parallel()

client, _, _, _ := newTestServer(t, testServerOptions{
Storer: mockstorer.New(),
Post: mockpost.New(mockpost.WithAcceptAll()),
})

const maxValidLevel = redundancy.PARANOID

tests := []struct {
name string
level int
want *jsonhttp.StatusResponse
}{
{"minimum level (NONE) is valid", int(redundancy.NONE), nil},
{"maximum valid level (PARANOID) is valid", int(maxValidLevel), nil},
{
"level below minimum is invalid", int(-1),
&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid header params",
Reasons: []jsonhttp.Reason{
{
Field: "Swarm-Redundancy-Level",
Error: "invalid syntax",
},
},
},
},
{
"level above maximum is invalid", int(maxValidLevel + 1),
&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid header params",
Reasons: []jsonhttp.Reason{
{
Field: "swarm-redundancy-level",
Error: fmt.Sprintf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)),
},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := []jsonhttptest.Option{
jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/plain"),
jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"),
jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, strconv.Itoa(tt.level)),
jsonhttptest.WithRequestBody(bytes.NewReader([]byte("test"))),
}
var statusCode int
if tt.want == nil {
statusCode = http.StatusCreated
} else {
statusCode = tt.want.Code
opts = append(opts, jsonhttptest.WithExpectedJSONResponse(*tt.want))
}
jsonhttptest.Request(t, client, http.MethodPost, "/bzz", statusCode, opts...)
})
}
}

func TestBzzDownloadRedundancyLevel(t *testing.T) {
t.Parallel()

client, _, _, _ := newTestServer(t, testServerOptions{
Storer: mockstorer.New(),
Post: mockpost.New(mockpost.WithAcceptAll()),
})

testData := []byte("test download redundancy level")
var resp api.BzzUploadResponse
jsonhttptest.Request(t, client, http.MethodPost, "/bzz", http.StatusCreated,
jsonhttptest.WithRequestHeader(api.ContentTypeHeader, "text/plain"),
jsonhttptest.WithRequestHeader(api.SwarmDeferredUploadHeader, "true"),
jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr),
jsonhttptest.WithRequestBody(bytes.NewReader(testData)),
jsonhttptest.WithUnmarshalJSONResponse(&resp),
)

const maxValidLevel = redundancy.PARANOID

tests := []struct {
name string
level int
want *jsonhttp.StatusResponse
}{
{"minimum level (NONE) is valid", int(redundancy.NONE), nil},
{"maximum valid level (PARANOID) is valid", int(maxValidLevel), nil},
{
"level below minimum is invalid", int(-1),
&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid header params",
Reasons: []jsonhttp.Reason{
{
Field: "Swarm-Redundancy-Level",
Error: "invalid syntax",
},
},
},
},
{
"level above maximum is invalid", int(maxValidLevel + 1),
&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid header params",
Reasons: []jsonhttp.Reason{
{
Field: "swarm-redundancy-level",
Error: fmt.Sprintf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID)),
},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := []jsonhttptest.Option{
jsonhttptest.WithRequestHeader(api.SwarmRedundancyLevelHeader, strconv.Itoa(tt.level)),
}
var statusCode int
if tt.want == nil {
statusCode = http.StatusOK
opts = append(opts,
jsonhttptest.WithExpectedResponse(testData),
)
} else {
statusCode = tt.want.Code
opts = append(opts, jsonhttptest.WithExpectedJSONResponse(*tt.want))
}
jsonhttptest.Request(t, client, http.MethodGet, "/bzz/"+resp.Reference.String(), statusCode, opts...)
})
}
}
34 changes: 34 additions & 0 deletions pkg/api/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2026 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package api

import (
"fmt"

"github.com/ethersphere/bee/v2/pkg/file/redundancy"
"github.com/go-playground/validator/v10"
)

const (
RedundancyLevelTag = "rLevel"
)

// setupValidation configures custom validation rules and their custom error messages.
func (s *Service) setupValidation() {
err := s.validate.RegisterValidation(RedundancyLevelTag, func(fl validator.FieldLevel) bool {
level := redundancy.Level(fl.Field().Uint())
return level.Validate()
})
if err != nil {
s.logger.Error(err, "failed to register validation")
panic(err)
}

s.customValidationMessages = map[string]func(err validator.FieldError) error{
RedundancyLevelTag: func(err validator.FieldError) error {
return fmt.Errorf("want redundancy level to be between %d and %d", int(redundancy.NONE), int(redundancy.PARANOID))
},
}
}
5 changes: 5 additions & 0 deletions pkg/file/redundancy/level.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const (
PARANOID
)

// Validate validates the redundancy level
func (l Level) Validate() bool {
return l >= NONE && l <= PARANOID
}

// GetParities returns number of parities based on appendix F table 5
func (l Level) GetParities(shards int) int {
et, err := l.getErasureTable()
Expand Down
Loading