Files
stack/components/ledger/internal/api/v2/controllers_accounts_test.go

324 lines
8.9 KiB
Go

package v2_test
import (
"bytes"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/formancehq/go-libs/time"
"github.com/formancehq/go-libs/auth"
"github.com/formancehq/go-libs/bun/bunpaginate"
sharedapi "github.com/formancehq/go-libs/api"
"github.com/formancehq/go-libs/metadata"
"github.com/formancehq/go-libs/query"
ledger "github.com/formancehq/ledger/internal"
v2 "github.com/formancehq/ledger/internal/api/v2"
"github.com/formancehq/ledger/internal/engine/command"
"github.com/formancehq/ledger/internal/opentelemetry/metrics"
"github.com/formancehq/ledger/internal/storage/ledgerstore"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestGetAccounts(t *testing.T) {
t.Parallel()
type testCase struct {
name string
queryParams url.Values
body string
expectQuery ledgerstore.PaginatedQueryOptions[ledgerstore.PITFilterWithVolumes]
expectStatusCode int
expectedErrorCode string
}
before := time.Now()
testCases := []testCase{
{
name: "nominal",
expectQuery: ledgerstore.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{
PITFilter: ledgerstore.PITFilter{
PIT: &before,
},
}).
WithPageSize(v2.DefaultPageSize),
},
{
name: "using metadata",
body: `{"$match": { "metadata[roles]": "admin" }}`,
expectQuery: ledgerstore.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{
PITFilter: ledgerstore.PITFilter{
PIT: &before,
},
}).
WithQueryBuilder(query.Match("metadata[roles]", "admin")).
WithPageSize(v2.DefaultPageSize),
},
{
name: "using address",
body: `{"$match": { "address": "foo" }}`,
expectQuery: ledgerstore.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{
PITFilter: ledgerstore.PITFilter{
PIT: &before,
},
}).
WithQueryBuilder(query.Match("address", "foo")).
WithPageSize(v2.DefaultPageSize),
},
{
name: "using empty cursor",
queryParams: url.Values{
"cursor": []string{bunpaginate.EncodeCursor(ledgerstore.NewGetAccountsQuery(ledgerstore.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{})))},
},
expectQuery: ledgerstore.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{}),
},
{
name: "using invalid cursor",
queryParams: url.Values{
"cursor": []string{"XXX"},
},
expectStatusCode: http.StatusBadRequest,
expectedErrorCode: v2.ErrValidation,
},
{
name: "invalid page size",
queryParams: url.Values{
"pageSize": []string{"nan"},
},
expectStatusCode: http.StatusBadRequest,
expectedErrorCode: v2.ErrValidation,
},
{
name: "page size over maximum",
queryParams: url.Values{
"pageSize": []string{"1000000"},
},
expectQuery: ledgerstore.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{
PITFilter: ledgerstore.PITFilter{
PIT: &before,
},
}).
WithPageSize(v2.MaxPageSize),
},
{
name: "using balance filter",
body: `{"$lt": { "balance[USD/2]": 100 }}`,
expectQuery: ledgerstore.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{
PITFilter: ledgerstore.PITFilter{
PIT: &before,
},
}).
WithQueryBuilder(query.Lt("balance[USD/2]", float64(100))).
WithPageSize(v2.DefaultPageSize),
},
{
name: "using exists filter",
body: `{"$exists": { "metadata": "foo" }}`,
expectQuery: ledgerstore.NewPaginatedQueryOptions(ledgerstore.PITFilterWithVolumes{
PITFilter: ledgerstore.PITFilter{
PIT: &before,
},
}).
WithQueryBuilder(query.Exists("metadata", "foo")).
WithPageSize(v2.DefaultPageSize),
},
{
name: "using invalid query payload",
body: `[]`,
expectStatusCode: http.StatusBadRequest,
expectedErrorCode: v2.ErrValidation,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
if testCase.expectStatusCode == 0 {
testCase.expectStatusCode = http.StatusOK
}
expectedCursor := bunpaginate.Cursor[ledger.ExpandedAccount]{
Data: []ledger.ExpandedAccount{
{
Account: ledger.Account{
Address: "world",
Metadata: metadata.Metadata{},
},
},
},
}
backend, mockLedger := newTestingBackend(t, true)
if testCase.expectStatusCode < 300 && testCase.expectStatusCode >= 200 {
mockLedger.EXPECT().
GetAccountsWithVolumes(gomock.Any(), ledgerstore.NewGetAccountsQuery(testCase.expectQuery)).
Return(&expectedCursor, nil)
}
router := v2.NewRouter(backend, nil, metrics.NewNoOpRegistry(), auth.NewNoAuth(), testing.Verbose())
req := httptest.NewRequest(http.MethodGet, "/xxx/accounts?pit="+before.Format(time.RFC3339Nano), bytes.NewBufferString(testCase.body))
rec := httptest.NewRecorder()
params := url.Values{}
if testCase.queryParams != nil {
params = testCase.queryParams
}
params.Set("pit", before.Format(time.RFC3339Nano))
req.URL.RawQuery = params.Encode()
router.ServeHTTP(rec, req)
require.Equal(t, testCase.expectStatusCode, rec.Code)
if testCase.expectStatusCode < 300 && testCase.expectStatusCode >= 200 {
cursor := sharedapi.DecodeCursorResponse[ledger.ExpandedAccount](t, rec.Body)
require.Equal(t, expectedCursor, *cursor)
} else {
err := sharedapi.ErrorResponse{}
sharedapi.Decode(t, rec.Body, &err)
require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode)
}
})
}
}
func TestGetAccount(t *testing.T) {
t.Parallel()
account := ledger.ExpandedAccount{
Account: ledger.Account{
Address: "foo",
Metadata: metadata.Metadata{},
},
}
now := time.Now()
query := ledgerstore.NewGetAccountQuery("foo")
query.PIT = &now
backend, mock := newTestingBackend(t, true)
mock.EXPECT().
GetAccountWithVolumes(gomock.Any(), query).
Return(&account, nil)
router := v2.NewRouter(backend, nil, metrics.NewNoOpRegistry(), auth.NewNoAuth(), testing.Verbose())
req := httptest.NewRequest(http.MethodGet, "/xxx/accounts/foo?pit="+now.Format(time.RFC3339Nano), nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
response, _ := sharedapi.DecodeSingleResponse[ledger.ExpandedAccount](t, rec.Body)
require.Equal(t, account, response)
}
func TestGetAccountWithEncoded(t *testing.T) {
t.Parallel()
account := ledger.ExpandedAccount{
Account: ledger.Account{
Address: "foo:bar",
Metadata: metadata.Metadata{},
},
}
now := time.Now()
query := ledgerstore.NewGetAccountQuery("foo:bar")
query.PIT = &now
backend, mock := newTestingBackend(t, true)
mock.EXPECT().
GetAccountWithVolumes(gomock.Any(), query).
Return(&account, nil)
router := v2.NewRouter(backend, nil, metrics.NewNoOpRegistry(), auth.NewNoAuth(), testing.Verbose())
req := httptest.NewRequest(http.MethodGet, "/xxx/accounts/foo%3Abar?pit="+now.Format(time.RFC3339Nano), nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
response, _ := sharedapi.DecodeSingleResponse[ledger.ExpandedAccount](t, rec.Body)
require.Equal(t, account, response)
}
func TestPostAccountMetadata(t *testing.T) {
t.Parallel()
type testCase struct {
name string
queryParams url.Values
expectStatusCode int
expectedErrorCode string
account string
body any
}
testCases := []testCase{
{
name: "nominal",
account: "world",
body: metadata.Metadata{
"foo": "bar",
},
},
{
name: "nominal",
account: "test-",
body: metadata.Metadata{
"foo": "bar",
},
},
{
name: "nominal",
account: "-t--e-st-",
body: metadata.Metadata{
"foo": "bar",
},
},
{
name: "invalid body",
account: "world",
body: "invalid - not an object",
expectStatusCode: http.StatusBadRequest,
expectedErrorCode: v2.ErrValidation,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
if testCase.expectStatusCode == 0 {
testCase.expectStatusCode = http.StatusNoContent
}
backend, mock := newTestingBackend(t, true)
if testCase.expectStatusCode == http.StatusNoContent {
mock.EXPECT().
SaveMeta(gomock.Any(), command.Parameters{}, ledger.MetaTargetTypeAccount, testCase.account, testCase.body).
Return(nil)
}
router := v2.NewRouter(backend, nil, metrics.NewNoOpRegistry(), auth.NewNoAuth(), testing.Verbose())
req := httptest.NewRequest(http.MethodPost, "/xxx/accounts/"+testCase.account+"/metadata", sharedapi.Buffer(t, testCase.body))
rec := httptest.NewRecorder()
req.URL.RawQuery = testCase.queryParams.Encode()
router.ServeHTTP(rec, req)
require.Equal(t, testCase.expectStatusCode, rec.Code)
if testCase.expectStatusCode >= 300 || testCase.expectStatusCode < 200 {
err := sharedapi.ErrorResponse{}
sharedapi.Decode(t, rec.Body, &err)
require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode)
}
})
}
}