@@ -2,6 +2,7 @@ package httphandler
22
33import (
44 "context"
5+ crand "crypto/rand"
56 "encoding/json"
67 "fmt"
78 "io"
@@ -15,6 +16,9 @@ import (
1516 "github.com/stretchr/testify/assert"
1617 "github.com/stretchr/testify/require"
1718
19+ "github.com/stellar/go/keypair"
20+ "github.com/stellar/go/strkey"
21+
1822 "github.com/stellar/stellar-disbursement-platform-backend/db"
1923 "github.com/stellar/stellar-disbursement-platform-backend/db/dbtest"
2024 "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
@@ -398,3 +402,140 @@ func Test_ReceiverWalletsHandler_PatchReceiverWallet_DuplicateStellarAddress(t *
398402 assert .Contains (t , string (respBody ), "Receiver wallet does not belong to the specified receiver" )
399403 })
400404}
405+
406+ func Test_ReceiverwalletsHandler_PatchReceiverWallet_MemoValidation (t * testing.T ) {
407+ dbt := dbtest .Open (t )
408+ defer dbt .Close ()
409+
410+ dbConnectionPool , err := db .OpenDBConnectionPool (dbt .DSN )
411+ require .NoError (t , err )
412+ defer dbConnectionPool .Close ()
413+
414+ models , err := data .NewModels (dbConnectionPool )
415+ require .NoError (t , err )
416+ tnt := schema.Tenant {ID : "tenant-id" }
417+ ctx := sdpcontext .SetTenantInContext (context .Background (), & tnt )
418+
419+ handler := ReceiverWalletsHandler {Models : models }
420+ router := chi .NewRouter ()
421+ router .Patch ("/receivers/{receiver_id}/wallets/{receiver_wallet_id}" , handler .PatchReceiverWallet )
422+
423+ createUserManagedReceiverWallet := func (t * testing.T , status data.ReceiversWalletStatus ) (* data.Receiver , * data.ReceiverWallet ) {
424+ t .Helper ()
425+
426+ wallet := data .CreateWalletFixture (t , ctx , dbConnectionPool , "User Managed Wallet" , "stellar.org" , "stellar.org" , "stellar://" )
427+ data .MakeWalletUserManaged (t , ctx , dbConnectionPool , wallet .ID )
428+ receiver := data .CreateReceiverFixture (t , ctx , dbConnectionPool , & data.Receiver {})
429+ rw := data .CreateReceiverWalletFixture (t , ctx , dbConnectionPool , receiver .ID , wallet .ID , status )
430+
431+ return receiver , rw
432+ }
433+
434+ doPatch := func (body string , receiverID string , receiverWalletID string ) (* http.Response , []byte ) {
435+ req , requestErr := http .NewRequestWithContext (ctx , http .MethodPatch ,
436+ fmt .Sprintf ("/receivers/%s/wallets/%s" , receiverID , receiverWalletID ), strings .NewReader (body ))
437+ require .NoError (t , requestErr )
438+ req .Header .Set ("Content-Type" , "application/json" )
439+
440+ rr := httptest .NewRecorder ()
441+ router .ServeHTTP (rr , req )
442+
443+ resp := rr .Result ()
444+ payload , readErr := io .ReadAll (resp .Body )
445+ require .NoError (t , readErr )
446+
447+ return resp , payload
448+ }
449+
450+ generateAccountAddress := func (t * testing.T ) string {
451+ t .Helper ()
452+ return keypair .MustRandom ().Address ()
453+ }
454+
455+ generateContractAddress := func (t * testing.T ) string {
456+ t .Helper ()
457+
458+ payload := make ([]byte , 32 )
459+ _ , randErr := crand .Read (payload )
460+ require .NoError (t , randErr )
461+
462+ addr , encodeErr := strkey .Encode (strkey .VersionByteContract , payload )
463+ require .NoError (t , encodeErr )
464+
465+ return addr
466+ }
467+
468+ t .Run ("accepts contract address without memo" , func (t * testing.T ) {
469+ receiver , rw := createUserManagedReceiverWallet (t , data .DraftReceiversWalletStatus )
470+ contractAddress := generateContractAddress (t )
471+
472+ resp , payload := doPatch (fmt .Sprintf (`{"stellar_address": "%s"}` , contractAddress ), receiver .ID , rw .ID )
473+ assert .Equal (t , http .StatusOK , resp .StatusCode )
474+
475+ var responseData map [string ]interface {}
476+ unmarshalErr := json .Unmarshal (payload , & responseData )
477+ require .NoError (t , unmarshalErr )
478+ assert .Equal (t , contractAddress , responseData ["stellar_address" ])
479+ })
480+
481+ t .Run ("allows switching from contract address to account address with memo" , func (t * testing.T ) {
482+ receiver , rw := createUserManagedReceiverWallet (t , data .DraftReceiversWalletStatus )
483+ currentContract := generateContractAddress (t )
484+
485+ resp , payload := doPatch (fmt .Sprintf (`{"stellar_address": "%s"}` , currentContract ), receiver .ID , rw .ID )
486+ require .Equal (t , http .StatusOK , resp .StatusCode , string (payload ))
487+
488+ newAccountAddress := generateAccountAddress (t )
489+ memo := "987654321"
490+
491+ resp , payload = doPatch (fmt .Sprintf (`{"stellar_address": "%s","stellar_memo":"%s"}` , newAccountAddress , memo ), receiver .ID , rw .ID )
492+ assert .Equal (t , http .StatusOK , resp .StatusCode )
493+
494+ var responseData map [string ]interface {}
495+ unmarshalErr := json .Unmarshal (payload , & responseData )
496+ require .NoError (t , unmarshalErr )
497+ assert .Equal (t , newAccountAddress , responseData ["stellar_address" ])
498+ assert .Equal (t , memo , responseData ["stellar_memo" ])
499+ assert .Equal (t , string (schema .MemoTypeID ), responseData ["stellar_memo_type" ])
500+ })
501+
502+ t .Run ("requires clearing memo before switching to contract address" , func (t * testing.T ) {
503+ receiver , rw := createUserManagedReceiverWallet (t , data .RegisteredReceiversWalletStatus )
504+ require .NotEmpty (t , rw .StellarMemo )
505+
506+ contractAddress := generateContractAddress (t )
507+
508+ resp , payload := doPatch (fmt .Sprintf (`{"stellar_address": "%s"}` , contractAddress ), receiver .ID , rw .ID )
509+ assert .Equal (t , http .StatusBadRequest , resp .StatusCode )
510+ assert .JSONEq (t , `{"error":"Clear memo before assigning a contract address"}` , string (payload ))
511+ })
512+
513+ t .Run ("rejects memo payload when assigning contract address" , func (t * testing.T ) {
514+ receiver , rw := createUserManagedReceiverWallet (t , data .DraftReceiversWalletStatus )
515+
516+ contractAddress := generateContractAddress (t )
517+
518+ resp , payload := doPatch (fmt .Sprintf (`{"stellar_address": "%s","stellar_memo":"memo-value"}` , contractAddress ), receiver .ID , rw .ID )
519+ assert .Equal (t , http .StatusBadRequest , resp .StatusCode )
520+ assert .JSONEq (t , `{"error":"Memos are not supported for contract addresses"}` , string (payload ))
521+ })
522+
523+ t .Run ("allows clearing memo when switching to contract address" , func (t * testing.T ) {
524+ receiver , rw := createUserManagedReceiverWallet (t , data .RegisteredReceiversWalletStatus )
525+ require .NotEmpty (t , rw .StellarMemo )
526+
527+ contractAddress := generateContractAddress (t )
528+
529+ resp , payload := doPatch (fmt .Sprintf (`{"stellar_address": "%s","stellar_memo":""}` , contractAddress ), receiver .ID , rw .ID )
530+ assert .Equal (t , http .StatusOK , resp .StatusCode )
531+
532+ var responseData map [string ]interface {}
533+ unmarshalErr := json .Unmarshal (payload , & responseData )
534+ require .NoError (t , unmarshalErr )
535+ assert .Equal (t , contractAddress , responseData ["stellar_address" ])
536+ _ , memoPresent := responseData ["stellar_memo" ]
537+ assert .False (t , memoPresent )
538+ _ , memoTypePresent := responseData ["stellar_memo_type" ]
539+ assert .False (t , memoTypePresent )
540+ })
541+ }
0 commit comments