Skip to content

Commit

Permalink
read endpoints for known-destinations (#55)
Browse files Browse the repository at this point in the history
* read endpoints for known-destinations

* mock event emitter

* fixed typo on anchorage endpoint

* fixed same typo in tests

* removed obligation for provider query, ask for connection-id header

* connection filter is a query parameter, use kebab case for endpoints, assert over full body
  • Loading branch information
Ptroger authored Dec 26, 2024
1 parent 1b769ed commit 51184fa
Show file tree
Hide file tree
Showing 12 changed files with 466 additions and 8 deletions.
341 changes: 341 additions & 0 deletions apps/vault/src/broker/__test__/e2e/known-destinations.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
import { EncryptionModuleOptionProvider } from '@narval/encryption-module'
import { LoggerModule, REQUEST_HEADER_CLIENT_ID } from '@narval/nestjs-shared'
import { Alg, generateJwk, privateKeyToHex } from '@narval/signature'
import { HttpStatus, INestApplication } from '@nestjs/common'
import { EventEmitter2 } from '@nestjs/event-emitter'
import { Test, TestingModule } from '@nestjs/testing'
import { mock } from 'jest-mock-extended'
import request from 'supertest'
import { v4 } from 'uuid'
import { ClientService } from '../../../client/core/service/client.service'
import { MainModule } from '../../../main.module'
import { ProvisionService } from '../../../provision.service'
import { KeyValueRepository } from '../../../shared/module/key-value/core/repository/key-value.repository'
import { InMemoryKeyValueRepository } from '../../../shared/module/key-value/persistence/repository/in-memory-key-value.repository'
import { TestPrismaService } from '../../../shared/module/persistence/service/test-prisma.service'
import { getTestRawAesKeyring } from '../../../shared/testing/encryption.testing'
import { ANCHORAGE_TEST_API_BASE_URL } from '../../core/service/__test__/integration/mocks/anchorage/server'
import { ConnectionService } from '../../core/service/connection.service'
import { KnownDestinationService } from '../../core/service/known-destination.service'
import { Provider } from '../../core/type/connection.type'
import { KnownDestination } from '../../core/type/indexed-resources.type'
import { getJwsd, testClient, testUserPrivateJwk } from '../util/mock-data'

describe('KnownDestination', () => {
let app: INestApplication
let module: TestingModule
let testPrismaService: TestPrismaService
let provisionService: ProvisionService
let clientService: ClientService
let knownDestinationService: KnownDestinationService
let connectionService: ConnectionService

let connection1Id: string
let connection2Id: string

const now = new Date('2025-01-01T00:00:00Z')

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [MainModule]
})
.overrideModule(LoggerModule)
.useModule(LoggerModule.forTest())
.overrideProvider(KeyValueRepository)
.useValue(new InMemoryKeyValueRepository())
.overrideProvider(EncryptionModuleOptionProvider)
.useValue({
keyring: getTestRawAesKeyring()
})
.overrideProvider(EventEmitter2)
.useValue(mock<EventEmitter2>())
.compile()

app = module.createNestApplication()
testPrismaService = module.get(TestPrismaService)
provisionService = module.get<ProvisionService>(ProvisionService)
clientService = module.get<ClientService>(ClientService)
connectionService = module.get<ConnectionService>(ConnectionService)

knownDestinationService = module.get<KnownDestinationService>(KnownDestinationService)
await testPrismaService.truncateAll()
})

afterAll(async () => {
await testPrismaService.truncateAll()
await module.close()
await app.close()
})

let knownDestinations: KnownDestination[]

beforeEach(async () => {
await testPrismaService.truncateAll()
await provisionService.provision()

await clientService.save(testClient)

await app.init()

const connection1 = await connectionService.create(testClient.clientId, {
connectionId: v4(),
provider: Provider.ANCHORAGE,
url: ANCHORAGE_TEST_API_BASE_URL,
createdAt: now,
credentials: {
apiKey: 'test-api-key',
privateKey: await privateKeyToHex(await generateJwk(Alg.EDDSA))
}
})

const connection2 = await connectionService.create(testClient.clientId, {
connectionId: v4(),
provider: Provider.ANCHORAGE,
url: ANCHORAGE_TEST_API_BASE_URL,
createdAt: now,
credentials: {
apiKey: 'test-api-key',
privateKey: await privateKeyToHex(await generateJwk(Alg.EDDSA))
}
})

connection1Id = connection1.connectionId
connection2Id = connection2.connectionId

knownDestinations = [
{
clientId: 'test-client-id',
provider: 'anchorage',
externalId: 'neverChanges',
externalClassification: null,
address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
label: null,
assetId: 'BTC',
networkId: 'BTC',
createdAt: now,
updatedAt: now,
connections: [connection1],
knownDestinationId: 'c2f7d2f1-e0b5-4966-a55f-7257420df81f'
},
{
clientId: 'test-client-id',
provider: 'anchorage',
externalId: 'toBeDeleted',
externalClassification: null,
address: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
label: '123456',
assetId: 'XRP',
networkId: 'XRP',
createdAt: now,
updatedAt: now,
connections: [connection1],
knownDestinationId: '9ba64a60-0684-4b7c-9d2d-78bf0a1c6de8'
},
{
clientId: 'test-client-id',
provider: 'anchorage',
externalId: 'toBeUpdated',
externalClassification: null,
address: '0x8Bc2B8F33e5AeF847B8973Fa669B948A3028D6bd',
label: null,
assetId: 'USDC',
networkId: 'ETH',
createdAt: now,
updatedAt: now,
connections: [connection2],
knownDestinationId: '8d5e8d6f-2836-4d47-821d-80906e1a1448'
},
{
clientId: 'test-client-id',
provider: 'anchorage',
externalId: 'toBeConnected',
externalClassification: null,
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
label: null,
assetId: 'ETH',
networkId: 'ETH',
createdAt: now,
updatedAt: now,
connections: [connection2],
knownDestinationId: '04817a66-039d-43e7-ab0a-023997597054'
}
]
await knownDestinationService.bulkCreate(knownDestinations)
})

describe('GET /provider/known-destinations', () => {
it('returns known destinations for a connection', async () => {
const { status, body } = await request(app.getHttpServer())
.get('/provider/known-destinations')
.set(REQUEST_HEADER_CLIENT_ID, testClient.clientId)
.query({ connectionId: connection1Id })
.set(
'detached-jws',
await getJwsd({
userPrivateJwk: testUserPrivateJwk,
requestUrl: `/provider/known-destinations?connectionId=${connection1Id}`,
payload: {},
htm: 'GET'
})
)

const expectedDestinations = [knownDestinations[0], knownDestinations[1]]

expect(body).toEqual({
data: expectedDestinations.map((knownDestination) => ({
...knownDestination,
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
connections: knownDestination.connections.map((connection) => ({
updatedAt: expect.any(String),
createdAt: expect.any(String),
status: connection.status,
provider: connection.provider,
url: connection.url,
connectionId: connection.connectionId,
clientId: connection.clientId
}))
})),
page: { next: null }
})
expect(status).toBe(HttpStatus.OK)
})

it('returns known destinations for another connection', async () => {
const { status, body } = await request(app.getHttpServer())
.get('/provider/known-destinations')
.set(REQUEST_HEADER_CLIENT_ID, testClient.clientId)
.query({ connectionId: connection2Id })
.set(
'detached-jws',
await getJwsd({
userPrivateJwk: testUserPrivateJwk,
requestUrl: `/provider/known-destinations?connectionId=${connection2Id}`,
payload: {},
htm: 'GET'
})
)

const expectedDestinations = [knownDestinations[2], knownDestinations[3]]
expect(body).toEqual({
data: expectedDestinations.map((knownDestination) => ({
...knownDestination,
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
connections: knownDestination.connections.map((connection) => ({
updatedAt: expect.any(String),
createdAt: expect.any(String),
status: connection.status,
provider: connection.provider,
url: connection.url,
connectionId: connection.connectionId,
clientId: connection.clientId
}))
})),
page: { next: null }
})
expect(status).toBe(HttpStatus.OK)
})

it('returns empty if connection is not in the system', async () => {
const { status, body } = await request(app.getHttpServer())
.get('/provider/known-destinations')
.set(REQUEST_HEADER_CLIENT_ID, testClient.clientId)
.query({ connectionId: 'unknown' })
.set(
'detached-jws',
await getJwsd({
userPrivateJwk: testUserPrivateJwk,
requestUrl: '/provider/known-destinations?connectionId=unknown',
payload: {},
htm: 'GET'
})
)

expect(body).toEqual({ data: [], page: { next: null } })
expect(status).toBe(HttpStatus.OK)
})

it('returns all known destinations if connection is not specified', async () => {
const { status, body } = await request(app.getHttpServer())
.get('/provider/known-destinations')
.set(REQUEST_HEADER_CLIENT_ID, testClient.clientId)
.set(
'detached-jws',
await getJwsd({
userPrivateJwk: testUserPrivateJwk,
requestUrl: '/provider/known-destinations',
payload: {},
htm: 'GET'
})
)

expect(body).toEqual({
data: knownDestinations.map((knownDestination) => ({
...knownDestination,
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
connections: knownDestination.connections.map((connection) => ({
updatedAt: expect.any(String),
createdAt: expect.any(String),
status: connection.status,
provider: connection.provider,
url: connection.url,
connectionId: connection.connectionId,
clientId: connection.clientId
}))
})),
page: { next: null }
})
expect(status).toBe(HttpStatus.OK)
})
})

describe('GET /provider/known-destinations/:knownDestinationId', () => {
it('returns known destination', async () => {
const { status, body } = await request(app.getHttpServer())
.get('/provider/known-destinations/c2f7d2f1-e0b5-4966-a55f-7257420df81f')
.set(REQUEST_HEADER_CLIENT_ID, testClient.clientId)
.set(
'detached-jws',
await getJwsd({
userPrivateJwk: testUserPrivateJwk,
requestUrl: `/provider/known-destinations/c2f7d2f1-e0b5-4966-a55f-7257420df81f`,
payload: {},
htm: 'GET'
})
)
expect(body.data).toEqual({
...knownDestinations[0],
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
connections: knownDestinations[0].connections.map((connection) => ({
updatedAt: expect.any(String),
createdAt: expect.any(String),
status: connection.status,
provider: connection.provider,
url: connection.url,
connectionId: connection.connectionId,
clientId: connection.clientId
}))
})
expect(status).toBe(HttpStatus.OK)
})

it('returns 404 for unknown known destination', async () => {
const { status, body } = await request(app.getHttpServer())
.get('/provider/known-destinations/unknown')
.set(REQUEST_HEADER_CLIENT_ID, testClient.clientId)
.set(
'detached-jws',
await getJwsd({
userPrivateJwk: testUserPrivateJwk,
requestUrl: `/provider/known-destinations/unknown`,
payload: {},
htm: 'GET'
})
)
expect(status).toBe(HttpStatus.NOT_FOUND)
})
})
})
4 changes: 3 additions & 1 deletion apps/vault/src/broker/broker.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { AddressService } from './core/service/address.service'
import { AnchorageSyncService } from './core/service/anchorage-sync.service'
import { AnchorageTransferService } from './core/service/anchorage-transfer.service'
import { ConnectionService } from './core/service/connection.service'
import { KnownDestinationService } from './core/service/know-destination.service'
import { KnownDestinationService } from './core/service/known-destination.service'
import { ProxyService } from './core/service/proxy.service'
import { SyncService } from './core/service/sync.service'
import { TransferPartyService } from './core/service/transfer-party.service'
Expand All @@ -25,6 +25,7 @@ import { AnchorageClient } from './http/client/anchorage.client'
import { ProviderAccountController } from './http/rest/controller/account.controller'
import { AddressController } from './http/rest/controller/address.controller'
import { ConnectionController } from './http/rest/controller/connection.controller'
import { KnownDestinationController } from './http/rest/controller/known-destination.controller'
import { ProxyController } from './http/rest/controller/proxy.controller'
import { SyncController } from './http/rest/controller/sync.controller'
import { TransferController } from './http/rest/controller/transfer.controller'
Expand All @@ -51,6 +52,7 @@ import { WalletRepository } from './persistence/repository/wallet.repository'
ProviderAccountController,
AddressController,
ConnectionController,
KnownDestinationController,
ProxyController,
SyncController,
TransferController,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { HttpStatus } from '@nestjs/common'
import { ApplicationExceptionParams } from '../../../shared/exception/application.exception'
import { BrokerException } from './broker.exception'

export class InvalidQueryStringException extends BrokerException {
constructor(query: string, params?: Partial<ApplicationExceptionParams>) {
super({
message: params?.message || `Query string "${query}" is required`,
suggestedHttpStatusCode: params?.suggestedHttpStatusCode || HttpStatus.BAD_REQUEST,
...params
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { AccountService } from '../../account.service'
import { AddressService } from '../../address.service'
import { AnchorageSyncService } from '../../anchorage-sync.service'
import { ConnectionService } from '../../connection.service'
import { KnownDestinationService } from '../../know-destination.service'
import { KnownDestinationService } from '../../known-destination.service'
import { WalletService } from '../../wallet.service'
import { trustedDestinationsHandlers, vaultHandlers } from './mocks/anchorage/handlers'
import { ANCHORAGE_TEST_API_BASE_URL, setupMockServer } from './mocks/anchorage/server'
Expand Down Expand Up @@ -217,7 +217,7 @@ describe(AnchorageSyncService.name, () => {
})

describe('syncKnownDestinations', () => {
it('fetches anchorage addresses and persist them as known-addresses', async () => {
it('fetches anchorage addresses and persist them as knownDestinations', async () => {
const { created } = await anchorageSyncService.syncKnownDestinations(connection)
const { data: knownDestinations } = await knownDestinationService.findAll(connection.clientId)

Expand Down
Loading

0 comments on commit 51184fa

Please sign in to comment.