Skip to content
Draft
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
39 changes: 39 additions & 0 deletions pkg/api/postage.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,45 @@ func (s *Service) estimateBatchTTL(batch *postage.Batch) (int64, error) {
return ttl.Int64(), nil
}

type postageLabelUpdateRequest struct {
Label string `json:"label"`
}

func (s *Service) postageUpdateLabelHandler(w http.ResponseWriter, r *http.Request) {
logger := s.logger.WithName("patch_stamp").Build()

paths := struct {
BatchID []byte `map:"batch_id" validate:"required,len=32"`
}{}
if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
response("invalid path params", logger, w)
return
}
hexBatchID := hex.EncodeToString(paths.BatchID)

body := postageLabelUpdateRequest{}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
logger.Debug("patch stamp: decode body failed", "batch_id", hexBatchID, "error", err)
logger.Error(nil, "patch stamp: decode body failed")
jsonhttp.BadRequest(w, "invalid request body")
return
}

if err := s.post.UpdateIssuerLabel(paths.BatchID, body.Label); err != nil {
logger.Debug("patch stamp: update label failed", "batch_id", hexBatchID, "error", err)
logger.Error(nil, "patch stamp: update label failed")
switch {
case errors.Is(err, postage.ErrNotFound):
jsonhttp.NotFound(w, "issuer does not exist")
default:
jsonhttp.InternalServerError(w, "cannot update label")
}
return
}

jsonhttp.OK(w, nil)
}

func (s *Service) postageTopUpHandler(w http.ResponseWriter, r *http.Request) {
logger := s.logger.WithName("patch_stamp_topup").Build()

Expand Down
118 changes: 118 additions & 0 deletions pkg/api/postage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1198,3 +1198,121 @@ func Test_postageDiluteHandler_invalidInputs(t *testing.T) {
})
}
}

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

batchID := batchOk
batchIDStr := batchOkStr
updatePath := "/stamps/" + batchIDStr

t.Run("ok", func(t *testing.T) {
t.Parallel()

si := postage.NewStampIssuer("original", "test identity", batchID, big.NewInt(3), 24, 6, 1000, false)
mp := mockpost.New(mockpost.WithIssuer(si))
ts, _, _, _ := newTestServer(t, testServerOptions{
Post: mp,
})

jsonhttptest.Request(t, ts, http.MethodPatch, updatePath, http.StatusOK,
jsonhttptest.WithRequestHeader("Content-Type", "application/json"),
jsonhttptest.WithRequestBody(bytes.NewBufferString(`{"label":"updated"}`)),
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{Code: http.StatusOK, Message: "OK"}),
)
})

t.Run("not-found", func(t *testing.T) {
t.Parallel()

mp := mockpost.New()
ts, _, _, _ := newTestServer(t, testServerOptions{
Post: mp,
})

jsonhttptest.Request(t, ts, http.MethodPatch, updatePath, http.StatusNotFound,
jsonhttptest.WithRequestHeader("Content-Type", "application/json"),
jsonhttptest.WithRequestBody(bytes.NewBufferString(`{"label":"updated"}`)),
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{Code: http.StatusNotFound, Message: "issuer does not exist"}),
)
})

t.Run("invalid-body", func(t *testing.T) {
t.Parallel()

si := postage.NewStampIssuer("original", "test identity", batchID, big.NewInt(3), 24, 6, 1000, false)
mp := mockpost.New(mockpost.WithIssuer(si))
ts, _, _, _ := newTestServer(t, testServerOptions{
Post: mp,
})

jsonhttptest.Request(t, ts, http.MethodPatch, updatePath, http.StatusBadRequest,
jsonhttptest.WithRequestHeader("Content-Type", "application/json"),
jsonhttptest.WithRequestBody(bytes.NewBufferString(`not-json`)),
jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{Code: http.StatusBadRequest, Message: "invalid request body"}),
)
})
}

//nolint:tparallel
func Test_postageUpdateLabelHandler_invalidInputs(t *testing.T) {
t.Parallel()

client, _, _, _ := newTestServer(t, testServerOptions{})

tests := []struct {
name string
batchID string
want jsonhttp.StatusResponse
}{{
name: "batch_id - odd hex string",
batchID: "123",
want: jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid path params",
Reasons: []jsonhttp.Reason{
{
Field: "batch_id",
Error: api.ErrHexLength.Error(),
},
},
},
}, {
name: "batch_id - invalid hex character",
batchID: "123G",
want: jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid path params",
Reasons: []jsonhttp.Reason{
{
Field: "batch_id",
Error: api.HexInvalidByteError('G').Error(),
},
},
},
}, {
name: "batch_id - invalid length",
batchID: "1234",
want: jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid path params",
Reasons: []jsonhttp.Reason{
{
Field: "batch_id",
Error: "want len:32",
},
},
},
}}

//nolint:paralleltest
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
jsonhttptest.Request(t, client, http.MethodPatch, "/stamps/"+tc.batchID, tc.want.Code,
jsonhttptest.WithRequestHeader("Content-Type", "application/json"),
jsonhttptest.WithRequestBody(bytes.NewBufferString(`{"label":"x"}`)),
jsonhttptest.WithExpectedJSONResponse(tc.want),
)
})
}
}
3 changes: 2 additions & 1 deletion pkg/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,8 @@ func (s *Service) mountBusinessDebug() {
s.checkChainAvailability,
s.postageSyncStatusCheckHandler,
web.FinalHandler(jsonhttp.MethodHandler{
"GET": http.HandlerFunc(s.postageGetStampHandler),
"GET": http.HandlerFunc(s.postageGetStampHandler),
"PATCH": http.HandlerFunc(s.postageUpdateLabelHandler),
})),
)

Expand Down
6 changes: 3 additions & 3 deletions pkg/api/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestEndpointOptions(t *testing.T) {
{"/wallet", []string{"GET"}, http.StatusNoContent},
{"/wallet/withdraw/{coin}", []string{"POST"}, http.StatusNoContent},
{"/stamps", []string{"GET"}, http.StatusNoContent},
{"/stamps/{batch_id}", []string{"GET"}, http.StatusNoContent},
{"/stamps/{batch_id}", []string{"GET", "PATCH"}, http.StatusNoContent},
{"/stamps/{batch_id}/buckets", []string{"GET"}, http.StatusNoContent},
{"/stamps/{amount}/{depth}", []string{"POST"}, http.StatusNoContent},
{"/stamps/topup/{batch_id}/{amount}", []string{"PATCH"}, http.StatusNoContent},
Expand Down Expand Up @@ -293,7 +293,7 @@ func TestEndpointOptions(t *testing.T) {
{"/wallet", nil, http.StatusForbidden},
{"/wallet/withdraw/{coin}", nil, http.StatusForbidden},
{"/stamps", []string{"GET"}, http.StatusNoContent},
{"/stamps/{batch_id}", []string{"GET"}, http.StatusNoContent},
{"/stamps/{batch_id}", []string{"GET", "PATCH"}, http.StatusNoContent},
{"/stamps/{batch_id}/buckets", []string{"GET"}, http.StatusNoContent},
{"/stamps/{amount}/{depth}", []string{"POST"}, http.StatusNoContent},
{"/stamps/topup/{batch_id}/{amount}", []string{"PATCH"}, http.StatusNoContent},
Expand Down Expand Up @@ -388,7 +388,7 @@ func TestEndpointOptions(t *testing.T) {
{"/wallet", nil, http.StatusForbidden},
{"/wallet/withdraw/{coin}", nil, http.StatusForbidden},
{"/stamps", []string{"GET"}, http.StatusNoContent},
{"/stamps/{batch_id}", []string{"GET"}, http.StatusNoContent},
{"/stamps/{batch_id}", []string{"GET", "PATCH"}, http.StatusNoContent},
{"/stamps/{batch_id}/buckets", []string{"GET"}, http.StatusNoContent},
{"/stamps/{amount}/{depth}", []string{"POST"}, http.StatusNoContent},
{"/stamps/topup/{batch_id}/{amount}", []string{"PATCH"}, http.StatusNoContent},
Expand Down
13 changes: 13 additions & 0 deletions pkg/postage/mock/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ func (m *mockPostage) IssuerUsable(_ *postage.StampIssuer) bool {
return true
}

func (m *mockPostage) UpdateIssuerLabel(id []byte, label string) error {
m.issuerLock.Lock()
defer m.issuerLock.Unlock()

_, exists := m.issuersMap[string(id)]
if !exists {
return postage.ErrNotFound
}
// label is local metadata only, real persistence is handled by the service implementation.
_ = label
return nil
}

func (m *mockPostage) HandleCreate(_ *postage.Batch, _ *big.Int) error { return nil }

func (m *mockPostage) HandleTopUp(_ []byte, _ *big.Int) {}
Expand Down
15 changes: 15 additions & 0 deletions pkg/postage/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Service interface {
StampIssuers() []*StampIssuer
GetStampIssuer([]byte) (*StampIssuer, func() error, error)
IssuerUsable(*StampIssuer) bool
UpdateIssuerLabel([]byte, string) error
BatchEventListener
BatchExpiryHandler
io.Closer
Expand Down Expand Up @@ -247,6 +248,20 @@ func (ps *service) GetStampIssuer(batchID []byte) (*StampIssuer, func() error, e
return nil, nil, ErrNotFound
}

// UpdateIssuerLabel updates the label of the stamp issuer with the given batch ID and persists the change.
func (ps *service) UpdateIssuerLabel(batchID []byte, label string) error {
ps.mtx.Lock()
defer ps.mtx.Unlock()

for _, st := range ps.issuers {
if bytes.Equal(batchID, st.data.BatchID) {
st.data.Label = label
return ps.save(st)
}
}
return ErrNotFound
}

// save persists the specified stamp issuer to the stamperstore.
func (ps *service) save(st *StampIssuer) error {
st.mtx.Lock()
Expand Down
Loading