Skip to content

Commit

Permalink
feat(SPV-1249) paymail list & assert with JSON template (#793)
Browse files Browse the repository at this point in the history
  • Loading branch information
chris-4chain authored Dec 5, 2024
1 parent c8bd64f commit 492beda
Show file tree
Hide file tree
Showing 18 changed files with 1,100 additions and 87 deletions.
12 changes: 12 additions & 0 deletions actions/paymails/routes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package paymails

import (
"github.com/bitcoin-sv/spv-wallet/server/handlers"
routes "github.com/bitcoin-sv/spv-wallet/server/handlers"
)

// RegisterRoutes creates the specific package routes in RESTful style
func RegisterRoutes(handlersManager *routes.Manager) {
group := handlersManager.Group(routes.GroupAPI, "/paymails")
group.GET("", handlers.AsUser(paymailAddressesSearch))
}
66 changes: 66 additions & 0 deletions actions/paymails/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package paymails

import (
"net/http"

"github.com/bitcoin-sv/spv-wallet/actions/common"
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/internal/query"
"github.com/bitcoin-sv/spv-wallet/mappings"
"github.com/bitcoin-sv/spv-wallet/models/filter"
"github.com/bitcoin-sv/spv-wallet/models/response"
"github.com/bitcoin-sv/spv-wallet/server/reqctx"
"github.com/gin-gonic/gin"
)

// paymailAddressesSearch will fetch a list of paymail addresses filtered by metadata
// Paymail addresses search by metadata
// @Summary Paymail addresses search
// @Description Paymail addresses search
// @Tags Users
// @Produce json
// @Param SearchPaymails body filter.PaymailFilter false "Supports targeted resource searches with filters and metadata, plus options for pagination and sorting to streamline data exploration and analysis"
// @Success 200 {object} []response.PaymailAddress "List of paymail addresses"
// @Failure 400 "Bad request - Error while parsing SearchPaymails from request body"
// @Failure 500 "Internal server error - Error while searching for paymail addresses"
// @Router /api/v1/paymails [get]
// @Security x-auth-xpub
func paymailAddressesSearch(c *gin.Context, userContext *reqctx.UserContext) {
logger := reqctx.Logger(c)

searchParams, err := query.ParseSearchParams[filter.PaymailFilter](c)
if err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCannotParseQueryParams.WithTrace(err), logger)
return
}

conditions := searchParams.Conditions.ToDbConditions()
metadata := mappings.MapToMetadata(searchParams.Metadata)
pageOptions := mappings.MapToDbQueryParams(&searchParams.Page)

paymailAddresses, err := reqctx.Engine(c).GetPaymailAddressesByXPubID(
c,
userContext.GetXPubID(),
metadata,
conditions,
pageOptions,
)
if err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCouldNotFindPaymail.WithTrace(err), logger)
return
}

count, err := reqctx.Engine(c).GetPaymailAddressesCount(c.Request.Context(), metadata, conditions)
if err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCouldNotFindPaymail.WithTrace(err), logger)
return
}

paymailAddressContracts := common.MapToTypeContracts(paymailAddresses, mappings.MapToPaymailContract)

result := response.PageModel[response.PaymailAddress]{
Content: paymailAddressContracts,
Page: common.GetPageDescriptionFromSearchParams(pageOptions, count),
}
c.JSON(http.StatusOK, result)
}
156 changes: 156 additions & 0 deletions actions/paymails/search_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package paymails_test

import (
"strings"
"testing"

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

func TestCurrentUserPaymails(t *testing.T) {
t.Run("return paymails info for user (single paymail)", func(t *testing.T) {
// given:
given, then := testabilities.New(t)
cleanup := given.StartedSPVWallet()
defer cleanup()
client := given.HttpClient().ForGivenUser(fixtures.Sender)

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

// 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": strings.ToLower(fixtures.Sender.Paymails[0]),
"PublicName": fixtures.Sender.Paymails[0],
"Alias": getAliasFromPaymail(t, fixtures.Sender.Paymails[0]),
"XPubID": fixtures.Sender.XPubID(),
"Domain": fixtures.PaymailDomain,
})
})

t.Run("return paymails info for user (multiple paymails)", func(t *testing.T) {
// given:
given, then := testabilities.New(t)
cleanup := given.StartedSPVWallet()
defer cleanup()
client := given.HttpClient().ForGivenUser(fixtures.UserWithMorePaymails)

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

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

t.Run("try to return paymails info for admin", func(t *testing.T) {
// given:
given, then := testabilities.New(t)
cleanup := given.StartedSPVWallet()
defer cleanup()
client := given.HttpClient().ForAdmin()

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

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

t.Run("return xpub info for anonymous", func(t *testing.T) {
// given:
given, then := testabilities.New(t)
cleanup := given.StartedSPVWallet()
defer cleanup()
client := given.HttpClient().ForAnonymous()

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

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

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
}
2 changes: 2 additions & 0 deletions actions/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/bitcoin-sv/spv-wallet/actions/contacts"
"github.com/bitcoin-sv/spv-wallet/actions/destinations"
"github.com/bitcoin-sv/spv-wallet/actions/merkleroots"
"github.com/bitcoin-sv/spv-wallet/actions/paymails"
"github.com/bitcoin-sv/spv-wallet/actions/sharedconfig"
"github.com/bitcoin-sv/spv-wallet/actions/transactions"
"github.com/bitcoin-sv/spv-wallet/actions/users"
Expand All @@ -24,6 +25,7 @@ func Register(appConfig *config.AppConfig, handlersManager *handlers.Manager) {
transactions.RegisterRoutes(handlersManager)
utxos.RegisterRoutes(handlersManager)
users.RegisterRoutes(handlersManager)
paymails.RegisterRoutes(handlersManager)
sharedconfig.RegisterRoutes(handlersManager)
merkleroots.RegisterRoutes(handlersManager)
contacts.RegisterRoutes(handlersManager)
Expand Down
7 changes: 7 additions & 0 deletions actions/testabilities/assert_spvwallet_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/bitcoin-sv/spv-wallet/actions/testabilities/apierror"
"github.com/bitcoin-sv/spv-wallet/engine/tester/jsonrequire"
"github.com/go-resty/resty/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -21,6 +22,7 @@ type SPVWalletResponseAssertions interface {
IsOK() SPVWalletResponseAssertions
HasStatus(status int) SPVWalletResponseAssertions
WithJSONf(expectedFormat string, args ...any)
WithJSONMatching(expectedTemplateFormat string, params map[string]any)
// IsUnauthorized asserts that the response status code is 401 and the error is about lack of authorization.
IsUnauthorized()
// IsUnauthorizedForAdmin asserts that the response status code is 401 and the error is that admin is not authorized to use the endpoint.
Expand Down Expand Up @@ -78,6 +80,11 @@ func (a *responseAssertions) WithJSONf(expectedFormat string, args ...any) {
a.assertJSONBody(expectedFormat, args...)
}

func (a *responseAssertions) WithJSONMatching(expectedTemplateFormat string, params map[string]any) {
a.assertJSONContentType()
jsonrequire.Match(a.t, expectedTemplateFormat, params, a.response.String())
}

func (a *responseAssertions) assertJSONContentType() {
contentType := a.response.Header().Get("Content-Type")
mediaType, _, err := mime.ParseMediaType(contentType)
Expand Down
88 changes: 88 additions & 0 deletions actions/users/get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package users_test

import (
"testing"

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

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

t.Run("return xpub info for user", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForUser()

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

// then:
then.Response(res).
IsOK().
WithJSONMatching(`{
"id": "{{.ID}}",
"createdAt": "{{ matchTimestamp }}",
"updatedAt": "{{ matchTimestamp }}",
"currentBalance": 0,
"deletedAt": null,
"metadata": "*",
"nextExternalNum": 1,
"nextInternalNum": 0
}`, map[string]any{
"ID": fixtures.Sender.XPubID(),
})
})

t.Run("return xpub info for user (old api)", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForUser()

// when:
res, _ := client.R().Get("/v1/xpub")

// then:
then.Response(res).
IsOK().
WithJSONMatching(`{
"id": "{{.ID}}",
"created_at": "/.*/",
"updated_at": "/.*/",
"current_balance": 0,
"deleted_at": null,
"metadata": "*",
"next_external_num": 1,
"next_internal_num": 0
}`, map[string]any{
"ID": fixtures.Sender.XPubID(),
})
})

t.Run("return xpub info for admin", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAdmin()

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

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

t.Run("return xpub info for anonymous", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAnonymous()

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

// then:
then.Response(res).IsUnauthorized()
})
}
Loading

0 comments on commit 492beda

Please sign in to comment.