Skip to content
Open
6 changes: 6 additions & 0 deletions actions/v2/admin/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ var ErrPaymailInconsistent = models.SPVError{Message: "inconsistent paymail addr

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

// ErrUserNotFound is when requested user does not exist in database
var ErrUserNotFound = models.SPVError{Message: "user not found", StatusCode: 404, Code: "error-user-not-found"}

// ErrGetUserFailed is when request for a user failed
var ErrGetUserFailed = models.SPVError{Message: "error fetching user", StatusCode: 500, Code: "error-user-fetch-failed"}
6 changes: 5 additions & 1 deletion actions/v2/admin/users/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ package users
import (
"net/http"

adminerrors "github.com/bitcoin-sv/spv-wallet/actions/v2/admin/errors"
"github.com/bitcoin-sv/spv-wallet/actions/v2/admin/internal/mapping"
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

// UserById returns a user by ID
func (s *APIAdminUsers) UserById(c *gin.Context, id string) {
user, err := s.engine.UsersService().GetByID(c, id)
if err != nil {
spverrors.ErrorResponse(c, err, s.logger)
spverrors.MapResponse(c, err, s.logger).
If(gorm.ErrRecordNotFound).Then(adminerrors.ErrUserNotFound).
Else(adminerrors.ErrGetUserFailed)
return
}

Expand Down
2 changes: 1 addition & 1 deletion actions/v2/users/current.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (s *APIUsers) CurrentUser(c *gin.Context) {
return
}

satoshis, err := reqctx.Engine(c).UsersService().GetBalance(c.Request.Context(), userID)
satoshis, err := s.usersService.GetBalance(c.Request.Context(), userID)
if err != nil {
spverrors.ErrorResponse(c, err, reqctx.Logger(c))
return
Expand Down
27 changes: 27 additions & 0 deletions actions/v2/users/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package users

import (
"net/http"

"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/server/reqctx"
"github.com/gin-gonic/gin"
)

// DeleteCurrentUser attempts to delete current user
func (s *APIUsers) DeleteCurrentUser(c *gin.Context) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe because only admin can create a user, maybe only admin should rather have power to remove the user

userContext := reqctx.GetUserContext(c)
userID, err := userContext.ShouldGetUserID()
if err != nil {
spverrors.ErrorResponse(c, err, reqctx.Logger(c))
return
}

err = s.usersService.Remove(c.Request.Context(), userID)
if err != nil {
spverrors.ErrorResponse(c, err, reqctx.Logger(c))
return
}

c.Status(http.StatusOK)
}
133 changes: 133 additions & 0 deletions actions/v2/users/remove_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package users_test

import (
"net/http"
"testing"

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

func TestCreateAndDeleteUser(t *testing.T) {
// given:
givenForAllTests := testabilities.Given(t)
cleanup := givenForAllTests.StartedSPVWalletWithConfiguration(
testengine.WithV2(),
)
defer cleanup()

// and:
userCandidate := fixtures.User{
PrivKey: "xprv9s21ZrQH143K3QFk3G7fGtpKfi6ws96DVzeXpvvZLUafPBwnpfbX7A343GU9jMrbvkoJR3UrdCKjwPhXrPAmzDfQ8ipo3zLryFvj2ABH1hn",
Paymails: []fixtures.Paymail{
"test_user2@" + fixtures.PaymailDomain,
},
}
publicKey := userCandidate.PublicKey().ToDERHex()

var testState struct {
userID string
}

t.Run("Create a 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{
"publicKey": publicKey,
"paymail": map[string]any{
"address": userCandidate.DefaultPaymail(),
},
}).
Post("/api/v2/admin/users")

// then:
then.Response(res).HasStatus(201)

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

t.Run("Get new user by id as admin", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
SetPathParam("id", testState.userID).
Get("/api/v2/admin/users/{id}")

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

t.Run("Delete user", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForGivenUser(userCandidate)

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

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

t.Run("Try to get new user by id as admin after deletion", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
SetPathParam("id", testState.userID).
Get("/api/v2/admin/users/{id}")

// then:
then.Response(res).HasStatus(http.StatusNotFound).WithJSONf(apierror.ExpectedJSON("error-user-not-found", "user not found"))
})

t.Run("Try to make a request as deleted user", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForGivenUser(userCandidate)

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

// then:
then.Response(res).HasStatus(http.StatusUnauthorized).WithJSONf(apierror.ExpectedJSON("error-unauthorized", "unauthorized"))
})
}

func TestDeleteUserWithUTXO(t *testing.T) {
// given:
given, then := testabilities.New(t)
cleanup := given.StartedSPVWalletWithConfiguration(
testengine.WithV2(),
)
defer cleanup()

// and:
userCandidate := fixtures.Sender

// and:
given.Faucet(fixtures.Sender).TopUp(1000)

// given:
client := given.HttpClient().ForGivenUser(userCandidate)

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

then.Response(res).HasStatus(http.StatusBadRequest).WithJSONf(apierror.ExpectedJSON("error-user-has-existing-utxos", "cannot delete user with existing UTXOs"))
}
16 changes: 12 additions & 4 deletions actions/v2/users/server.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
package users

import (
"context"

"github.com/bitcoin-sv/spv-wallet/engine"
"github.com/bitcoin-sv/spv-wallet/models/bsv"
"github.com/rs/zerolog"
)

type usersService interface {
Remove(ctx context.Context, userID string) error
GetBalance(ctx context.Context, userID string) (bsv.Satoshis, error)
}

// APIUsers represents server with API endpoints
type APIUsers struct {
engine engine.ClientInterface
logger *zerolog.Logger
usersService usersService
logger *zerolog.Logger
}

// NewAPIUsers creates a new server with API endpoints
func NewAPIUsers(engine engine.ClientInterface, log *zerolog.Logger) APIUsers {
logger := log.With().Str("api", "users").Logger()

return APIUsers{
engine: engine,
logger: &logger,
usersService: engine.UsersService(),
logger: &logger,
}
}
19 changes: 18 additions & 1 deletion api/endpoints/user.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ info:
title: ignored
version: ignored
paths:

/api/v2/users/current:
get:
operationId: currentUser
Expand All @@ -22,6 +21,24 @@ paths:
$ref: "../components/responses.yaml#/components/responses/UserNotAuthorized"
500:
$ref: "../components/responses.yaml#/components/responses/InternalServerError"
delete:
operationId: deleteCurrentUser
security:
- XPubAuth:
- "user"
tags:
- User
summary: Delete current user
description: >-
This endpoint will delete current user with associated paymails, addresses, operations and tracked outputs
You cannot delete user with balance larger than 0
responses:
200:
description: Success
401:
$ref: "../components/responses.yaml#/components/responses/UserNotAuthorized"
500:
$ref: "../components/responses.yaml#/components/responses/InternalServerError"

/api/v2/data/{id}:
get:
Expand Down
19 changes: 19 additions & 0 deletions api/gen.api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions api/gen.api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,22 @@ paths:
tags:
- Transactions
/api/v2/users/current:
delete:
description: This endpoint will delete current user with associated paymails, addresses, operations and tracked outputs You cannot delete user with balance larger than 0
operationId: deleteCurrentUser
responses:
"200":
description: Success
"401":
$ref: '#/components/responses/responses_UserNotAuthorized'
"500":
$ref: '#/components/responses/responses_InternalServerError'
security:
- XPubAuth:
- user
summary: Delete current user
tags:
- User
get:
description: This endpoint return balance of current authenticated user
operationId: currentUser
Expand Down
20 changes: 20 additions & 0 deletions api/manualtests/adminapi/admin_create_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,23 @@ func TestCreateUser(t *testing.T) {
}).
RequireSuccess()
}

func TestCreateUserAgain(t *testing.T) {
t.Skip("Don't run it yet")

manualtests.APICallForAdmin(t).
CallWithUpdateState(func(state manualtests.StateForCall, c *client.ClientWithResponses) (manualtests.Result, error) {
user := state.CurrentUser()

user.RemoveTag("deleted")

return c.CreateUserWithResponse(context.Background(), client.CreateUserJSONRequestBody{
Paymail: &client.RequestsAddPaymail{
Address: user.PaymailAddress(),
AvatarURL: lo.ToPtr(user.AvatarURL()),
PublicName: lo.ToPtr(user.PublicName()),
},
PublicKey: user.PublicKey,
})
})
}
Loading
Loading