Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
51e690f
feat(SPV-1538): implement upsert contact
pawellewandowski98 Feb 21, 2025
ceb7d4b
feat: add mapping for contact
pawellewandowski98 Feb 21, 2025
bc64687
feat(SPV-1538): add new contact endpoints
pawellewandowski98 Feb 25, 2025
e58bfab
feat: add admin contact endpoints
pawellewandowski98 Feb 25, 2025
e8ded44
feat: add accept and reject admin endpoints
pawellewandowski98 Feb 25, 2025
2e2850e
feat: update errors
pawellewandowski98 Feb 26, 2025
604d4c2
fix: fix api methods and regenerate
pawellewandowski98 Feb 26, 2025
aaf313f
Merge remote-tracking branch 'refs/remotes/origin/main' into feat/SPV…
pawellewandowski98 Mar 3, 2025
ed05818
chore: generate after merge
pawellewandowski98 Mar 3, 2025
8b056d2
test: add tests for admin endpoints
pawellewandowski98 Mar 4, 2025
0c72c52
tests: add eew tests for contacts
pawellewandowski98 Mar 4, 2025
ce2f845
tests: add contact test for the rest ofthe endpoints
pawellewandowski98 Mar 5, 2025
d6bb02a
Merge remote-tracking branch 'refs/remotes/origin/main' into feat/SPV…
pawellewandowski98 Mar 5, 2025
c1a04a4
fix: yaml linter
pawellewandowski98 Mar 5, 2025
d6a44ae
fix: linter errors
pawellewandowski98 Mar 5, 2025
f1d5f08
fix: linter errors
pawellewandowski98 Mar 5, 2025
c077248
fix: linter error
pawellewandowski98 Mar 5, 2025
22428a8
tests: refactor the way of creating endpoint url
pawellewandowski98 Mar 6, 2025
c1186bc
test: rename ContactFixture
pawellewandowski98 Mar 6, 2025
07d1058
tests: create new tests when needed
pawellewandowski98 Mar 6, 2025
67deaa0
chore: use gin context
pawellewandowski98 Mar 6, 2025
e085f6f
test: refactor the way of checking errors
pawellewandowski98 Mar 6, 2025
7da9cd8
chore: remove search endpoints
pawellewandowski98 Mar 7, 2025
ae36b57
Merge remote-tracking branch 'refs/remotes/origin/main' into feat/SPV…
pawellewandowski98 Mar 7, 2025
aada719
fix: fix creating api v2 after merge
pawellewandowski98 Mar 7, 2025
8e18e4d
fix: linter errors
pawellewandowski98 Mar 7, 2025
12b5e39
feat: use contacts service interface instead of engine
pawellewandowski98 Mar 7, 2025
df89efc
tests: fix tests
pawellewandowski98 Mar 7, 2025
c74a6c7
tests: fix tests
pawellewandowski98 Mar 7, 2025
7cc7c62
Merge branch 'main' into feat/SPV-1538-migrate-contacts
pawellewandowski98 Mar 10, 2025
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
1 change: 1 addition & 0 deletions actions/testabilities/assert_spvwallet_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type JsonValueGetter interface {
GetString(xpath string) string
GetAsType(xpath string, target any)
GetField(xpath string) any
GetInt(xpath string) int
}

func Then(t testing.TB, app SPVWalletApplicationFixture) SPVWalletApplicationAssertions {
Expand Down
6 changes: 6 additions & 0 deletions actions/testabilities/fixture_spvwallet_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type SPVWalletApplicationFixture interface {

Faucet(user fixtures.User) testengine.FaucetFixture

User(user fixtures.User) testengine.UserFixture

EngineFixture() testengine.EngineFixture

// Tx creates a new mocked transaction builder
Expand Down Expand Up @@ -162,6 +164,10 @@ func (f *appFixture) Faucet(user fixtures.User) testengine.FaucetFixture {
return f.engineFixture.Faucet(user)
}

func (f *appFixture) User(user fixtures.User) testengine.UserFixture {
return f.engineFixture.User(user)
}

func (f *appFixture) Tx() txtestability.TransactionSpec {
return f.engineFixture.Tx()
}
Expand Down
22 changes: 22 additions & 0 deletions actions/v2/admin/contacts/accept.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package contacts

import (
"net/http"

"github.com/bitcoin-sv/spv-wallet/actions/v2/internal/mapping"
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/gin-gonic/gin"
)

// AdminAcceptInvitation accepts an invitation from a contact.
func (s *APIAdminContacts) AdminAcceptInvitation(c *gin.Context, id uint) {
contact, err := s.contactsService.AcceptContactByID(c, id)
if err != nil {
spverrors.ErrorResponse(c, err, s.logger)
return
}

res := mapping.MapToContactContract(contact)

c.JSON(http.StatusOK, res)
}
131 changes: 131 additions & 0 deletions actions/v2/admin/contacts/accept_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package contacts_test

import (
"fmt"
"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"
"github.com/bitcoin-sv/spv-wallet/engine/v2/contacts/contactsmodels"
)

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

t.Run("Accept contact", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
contact := given.User(fixtures.Sender).HasContactTo(fixtures.RecipientInternal)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
Post(fmt.Sprintf("/api/v2/admin/invitations/%d", contact.ID))

// then:
then.Response(res).
HasStatus(200).
WithJSONMatching(`{
"id": "{{ matchNumber }}",
"createdAt": "{{ matchTimestamp }}",
"updatedAt": "{{ matchTimestamp }}",
"fullName": "{{ .fullName }}",
"paymail": "{{ .paymail }}",
"pubKey": "{{ matchHexWithLength 66 }}",
"status": "{{ .status }}"
}`, map[string]any{
"fullName": fixtures.RecipientInternal.DefaultPaymail().PublicName(),
"paymail": fixtures.RecipientInternal.DefaultPaymail().String(),
"status": contactsmodels.ContactNotConfirmed,
})
})

t.Run("Accept already accepted contact", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
contact := given.User(fixtures.Sender).HasContactTo(fixtures.RecipientInternal)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
Post(fmt.Sprintf("/api/v2/admin/invitations/%d", contact.ID))

// then:
then.Response(res).
HasStatus(200).
WithJSONMatching(`{
"id": "{{ matchNumber }}",
"createdAt": "{{ matchTimestamp }}",
"updatedAt": "{{ matchTimestamp }}",
"fullName": "{{ .fullName }}",
"paymail": "{{ .paymail }}",
"pubKey": "{{ matchHexWithLength 66 }}",
"status": "{{ .status }}"
}`, map[string]any{
"fullName": fixtures.RecipientInternal.DefaultPaymail().PublicName(),
"paymail": fixtures.RecipientInternal.DefaultPaymail().String(),
"status": contactsmodels.ContactNotConfirmed,
})

// when:
res, _ = client.R().
Post(fmt.Sprintf("/api/v2/admin/invitations/%d", contact.ID))

// then:
then.Response(res).
HasStatus(200).
WithJSONMatching(`{
"id": "{{ matchNumber }}",
"createdAt": "{{ matchTimestamp }}",
"updatedAt": "{{ matchTimestamp }}",
"fullName": "{{ .fullName }}",
"paymail": "{{ .paymail }}",
"pubKey": "{{ matchHexWithLength 66 }}",
"status": "{{ .status }}"
}`, map[string]any{
"fullName": fixtures.RecipientInternal.DefaultPaymail().PublicName(),
"paymail": fixtures.RecipientInternal.DefaultPaymail().String(),
"status": contactsmodels.ContactNotConfirmed,
})
})

t.Run("Accept contact with user xpub", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
contact := given.User(fixtures.Sender).HasContactTo(fixtures.RecipientInternal)

client := given.HttpClient().ForUser()

// when:
res, _ := client.R().
Post(fmt.Sprintf("/api/v2/admin/invitations/%d", contact.ID))

// then:
then.Response(res).
HasStatus(401).
WithJSONf(apierror.ExpectedJSON("error-unauthorized-xpub-not-an-admin-key", "xpub provided is not an admin key"))
})

t.Run("No contact to accept", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
Post("/api/v2/admin/invitations/99999")

// then:
then.Response(res).
HasStatus(500).
WithJSONf(apierror.ExpectedJSON("error-contact-updating-status-failed", "updating contact status failed"))

})
}
26 changes: 26 additions & 0 deletions actions/v2/admin/contacts/confirm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package contacts

import (
"net/http"

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

// AdminConfirmContact confirms a contact between two users.
func (s *APIAdminContacts) AdminConfirmContact(c *gin.Context) {
var reqParams *api.RequestsAdminConfirmContact
if err := c.Bind(&reqParams); err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest.WithTrace(err), s.logger)
return
}

if err := s.contactsService.AdminConfirmContacts(c, reqParams.PaymailA, reqParams.PaymailB); err != nil {
spverrors.ErrorResponse(c, err, s.logger)
return
}

c.Status(http.StatusOK)

}
130 changes: 130 additions & 0 deletions actions/v2/admin/contacts/confirm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package contacts_test

import (
"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 TestConfirmContact(t *testing.T) {
// given:
givenForAllTests := testabilities.Given(t)
cleanup := givenForAllTests.StartedSPVWalletWithConfiguration(
testengine.WithV2(),
)
defer cleanup()

t.Run("No side has contact", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)

client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
SetBody(map[string]any{
"paymailA": fixtures.Sender.DefaultPaymail().String(),
"paymailB": fixtures.RecipientInternal.DefaultPaymail().String(),
}).
Post("/api/v2/admin/contacts/confirmations")

// then:
then.Response(res).HasStatus(500).
WithJSONf(apierror.ExpectedJSON("error-contact-getting-contact-failed", "getting contact failed"))
})

t.Run("Only one side has contact", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
given.User(fixtures.Sender).HasContactTo(fixtures.RecipientInternal)

client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
SetBody(map[string]any{
"paymailA": fixtures.Sender.DefaultPaymail().String(),
"paymailB": fixtures.RecipientInternal.DefaultPaymail().String(),
}).
Post("/api/v2/admin/contacts/confirmations")

// then:
then.Response(res).HasStatus(500).
WithJSONf(apierror.ExpectedJSON("error-contact-getting-contact-failed", "getting contact failed"))
})

t.Run("Confirm contact", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
given.User(fixtures.Sender).HasContactTo(fixtures.RecipientInternal)
given.User(fixtures.RecipientInternal).HasContactTo(fixtures.Sender)

client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
SetBody(map[string]any{
"paymailA": fixtures.Sender.DefaultPaymail().String(),
"paymailB": fixtures.RecipientInternal.DefaultPaymail().String(),
}).
Post("/api/v2/admin/contacts/confirmations")

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

t.Run("Confirm already confirmed contact", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)
given.User(fixtures.Sender).HasContactTo(fixtures.RecipientInternal)
given.User(fixtures.RecipientInternal).HasContactTo(fixtures.Sender)

client := given.HttpClient().ForAdmin()

// when:
res, _ := client.R().
SetBody(map[string]any{
"paymailA": fixtures.Sender.DefaultPaymail().String(),
"paymailB": fixtures.RecipientInternal.DefaultPaymail().String(),
}).
Post("/api/v2/admin/contacts/confirmations")

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

// when:
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove unnecessary // and:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

res, _ = client.R().
SetBody(map[string]any{
"paymailA": fixtures.Sender.DefaultPaymail().String(),
"paymailB": fixtures.RecipientInternal.DefaultPaymail().String(),
}).
Post("/api/v2/admin/contacts/confirmations")

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

t.Run("Confirm contact with user xpub", func(t *testing.T) {
// given:
given, then := testabilities.NewOf(givenForAllTests, t)

client := given.HttpClient().ForUser()

// when:
res, _ := client.R().
SetBody(map[string]any{
"paymailA": fixtures.RecipientExternal.DefaultPaymail().String(),
"paymailB": fixtures.Sender.DefaultPaymail().String(),
}).
Post("/api/v2/admin/contacts/confirmations")

// then:
then.Response(res).
HasStatus(401).
WithJSONf(apierror.ExpectedJSON("error-unauthorized-xpub-not-an-admin-key", "xpub provided is not an admin key"))
})

}
35 changes: 35 additions & 0 deletions actions/v2/admin/contacts/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package contacts

import (
"net/http"

"github.com/bitcoin-sv/spv-wallet/actions/v2/internal/mapping"
"github.com/bitcoin-sv/spv-wallet/api"
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/engine/v2/contacts/contactsmodels"
"github.com/gin-gonic/gin"
)

// AdminCreateContact creates a new contact for a user.
func (s *APIAdminContacts) AdminCreateContact(c *gin.Context, paymail string) {
var req api.RequestsAdminCreateContact
if err := c.Bind(&req); err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest.WithTrace(err), s.logger)
return
}

newContact := contactsmodels.NewContact{
FullName: req.FullName,
NewContactPaymail: paymail,
RequesterPaymail: req.CreatorPaymail,
Status: contactsmodels.ContactNotConfirmed,
}

contact, err := s.contactsService.AdminCreateContact(c, newContact)
if err != nil {
spverrors.ErrorResponse(c, err, s.logger)
return
}

c.JSON(http.StatusOK, mapping.MapToContactContract(contact))
}
Loading
Loading