Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SPV-1248) add domain check for paymail entry #828

Merged
merged 9 commits into from
Jan 16, 2025
11 changes: 11 additions & 0 deletions actions/admin/paymail_addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package admin

import (
"net/http"
"slices"

"github.com/bitcoin-sv/go-paymail"
"github.com/bitcoin-sv/spv-wallet/actions/common"
"github.com/bitcoin-sv/spv-wallet/engine"
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
Expand Down Expand Up @@ -130,6 +132,15 @@ func paymailCreateAddress(c *gin.Context, _ *reqctx.AdminContext) {
opts = append(opts, engine.WithMetadatas(requestBody.Metadata))
}

config := reqctx.AppConfig(c)
dorzepowski marked this conversation as resolved.
Show resolved Hide resolved
if config.Paymail.DomainValidationEnabled {
_, actualDomain, _ := paymail.SanitizePaymail(requestBody.Address)
if !slices.Contains(config.Paymail.Domains, actualDomain) {
spverrors.ErrorResponse(c, spverrors.ErrInvalidDomain, logger)
return
}
}

var paymailAddress *engine.PaymailAddress
paymailAddress, err := reqctx.Engine(c).NewPaymailAddress(
c.Request.Context(), requestBody.Key, requestBody.Address, requestBody.PublicName, requestBody.Avatar, opts...)
Expand Down
11 changes: 11 additions & 0 deletions actions/admin/paymail_addresses_old.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package admin

import (
"net/http"
"slices"

"github.com/bitcoin-sv/go-paymail"
"github.com/bitcoin-sv/spv-wallet/engine"
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/mappings"
Expand Down Expand Up @@ -163,6 +165,15 @@ func paymailCreateAddressOld(c *gin.Context, _ *reqctx.AdminContext) {
opts = append(opts, engine.WithMetadatas(requestBody.Metadata))
}

config := reqctx.AppConfig(c)
if config.Paymail.DomainValidationEnabled {
_, actualDomain, _ := paymail.SanitizePaymail(requestBody.Address)
if !slices.Contains(config.Paymail.Domains, actualDomain) {
spverrors.ErrorResponse(c, spverrors.ErrInvalidDomain, logger)
return
}
}

var paymailAddress *engine.PaymailAddress
paymailAddress, err := reqctx.Engine(c).NewPaymailAddress(
c.Request.Context(), requestBody.Key, requestBody.Address, requestBody.PublicName, requestBody.Avatar, opts...)
Expand Down
237 changes: 237 additions & 0 deletions actions/admin/paymail_addresses_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package admin_test

import (
"net/http"
"strings"
"testing"

"github.com/bitcoin-sv/spv-wallet/actions/testabilities"
"github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures"
assert "github.com/stretchr/testify/require"
)

func TestGetPaymails(t *testing.T) {
// given:
givenForAllTests := testabilities.Given(t)
cleanup := givenForAllTests.StartedSPVWallet()
defer cleanup()

// and
userPaymail := strings.ToLower(fixtures.Sender.DefaultPaymail())
userAlias := getAliasFromPaymail(t, userPaymail)

var testState struct {
defaultPaymailID string
}

t.Run("get paymails for selected user as admin", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().Get("/api/v1/admin/paymails?alias=" + userAlias)

// then:
then.Response(res).
IsOK().
WithJSONMatching(`{
"content": [
{
"address": "{{.Address}}",
"alias": "{{.Alias}}",
"avatar": "{{ matchURL | orEmpty }}",
"createdAt": "{{ matchTimestamp }}",
"deletedAt": null,
"domain": "{{.Domain}}",
"id": "{{ matchID64 }}",
"metadata": "*",
"publicName": "{{.PublicName}}",
"updatedAt": "{{ matchTimestamp }}",
"xpubId": "{{.XPubID}}"
}
],
"page": {
"number": 1,
"size": 50,
"totalElements": 1,
"totalPages": 1
}
}`, map[string]any{
"Address": userPaymail,
"PublicName": userPaymail,
"Alias": userAlias,
"XPubID": fixtures.Sender.XPubID(),
"Domain": fixtures.PaymailDomain,
})

// update:
getter := then.Response(res).JSONValue()
testState.defaultPaymailID = getter.GetString("content[0]/id")
})

t.Run("get single paymail as admin", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().Get("/api/v1/admin/paymails/" + testState.defaultPaymailID)

// then:
then.Response(res).
IsOK().
WithJSONMatching(`{
"address": "{{.Address}}",
"alias": "{{.Alias}}",
"avatar": "{{ matchURL | orEmpty }}",
"createdAt": "{{ matchTimestamp }}",
"deletedAt": null,
"domain": "{{.Domain}}",
"id": "{{ matchID64 }}",
"metadata": "*",
"publicName": "{{.PublicName}}",
"updatedAt": "{{ matchTimestamp }}",
"xpubId": "{{.XPubID}}"
}`, map[string]any{
"Address": userPaymail,
"PublicName": userPaymail,
"Alias": userAlias,
"XPubID": fixtures.Sender.XPubID(),
"Domain": fixtures.PaymailDomain,
})
})

t.Run("try to search paymails as user", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForUser()

// when:
res, _ := client.R().Get("/api/v1/admin/paymails")

// then:
then.Response(res).IsUnauthorizedForUser()
})
}

func TestPaymailLivecycle(t *testing.T) {
// given:
givenForAllTests := testabilities.Given(t)
cleanup := givenForAllTests.StartedSPVWallet()
defer cleanup()

// and:
newAlias := "newalias"
newPaymail := newAlias + "@" + fixtures.PaymailDomain

var testState struct {
newPaymailID string
paymailDetailsRawBody []byte
}

t.Run("add paymail for selected user as admin", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
SetBody(map[string]any{
"key": fixtures.Sender.XPub(),
"address": newPaymail,
"publicName": newPaymail,
"avatar": "",
}).
Post("/api/v1/admin/paymails")

// then:
then.Response(res).
HasStatus(http.StatusCreated).
WithJSONMatching(`{
"address": "{{.Address}}",
"alias": "{{.Alias}}",
"avatar": "",
"createdAt": "{{ matchTimestamp }}",
"deletedAt": null,
"domain": "{{.Domain}}",
"id": "{{ matchID64 }}",
"metadata": null,
"publicName": "{{.PublicName}}",
"updatedAt": "{{ matchTimestamp }}",
"xpubId": "{{.XPubID}}"
}`, map[string]any{
"Address": newPaymail,
"PublicName": newPaymail,
"Alias": newAlias,
"XPubID": fixtures.Sender.XPubID(),
"Domain": fixtures.PaymailDomain,
})

// update:
getter := then.Response(res).JSONValue()
testState.newPaymailID = getter.GetString("id")
testState.paymailDetailsRawBody = res.Body()
})

t.Run("get added paymail as admin", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().Get("/api/v1/admin/paymails/" + testState.newPaymailID)

// then:
then.Response(res).
IsOK()

// and:
assert.Equal(t, testState.paymailDetailsRawBody, res.Body())
})

t.Run("remove paymail as admin", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
SetBody(map[string]any{
"address": newPaymail,
}).
Delete("/api/v1/admin/paymails/" + testState.newPaymailID)

// then:
then.Response(res).IsOK()

// verify paymail is deleted by trying to get it
getRes, _ := client.R().Get("/api/v1/admin/paymails/" + testState.newPaymailID)
then.Response(getRes).HasStatus(404)
})

t.Run("try to remove paymail as user", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForUser()

// when:
res, _ := client.R().
SetBody(map[string]any{
"address": newPaymail,
}).
Delete("/api/v1/admin/paymails/" + testState.newPaymailID)

// then:
then.Response(res).IsUnauthorizedForUser()
})
}

func getAliasFromPaymail(t testing.TB, paymail string) (alias string) {
parts := strings.SplitN(paymail, "@", 2)
if len(parts) == 0 {
t.Fatalf("Failed to parse paymail: %s", paymail)
}
alias = strings.ToLower(parts[0])
return
}
3 changes: 3 additions & 0 deletions engine/spverrors/definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,9 @@ var ErrMissingAddress = models.SPVError{Message: "missing required field: addres
// ErrMissingFieldScriptPubKey is when the field is required but missing
var ErrMissingFieldScriptPubKey = models.SPVError{Message: "missing required field: script_pub_key", StatusCode: 400, Code: "error-missing-field-script-pub-key"}

// ErrInvalidDomain is when the domain is wrong
var ErrInvalidDomain = models.SPVError{Message: "invalid domain", StatusCode: 400, Code: "error-invalid-domain"}

// ErrMissingFieldSatoshis is when the field satoshis is required but missing
var ErrMissingFieldSatoshis = models.SPVError{Message: "missing required field: satoshis", StatusCode: 400, Code: "error-missing-field-satoshis"}

Expand Down
Loading