refactor: split
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
state,
|
||||
pageStatus,
|
||||
uninstallExtension,
|
||||
getAdminSession,
|
||||
installLatestExtensionViaUi,
|
||||
enableExtensionViaUi,
|
||||
disableExtensionViaUi
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('001 extensions can be installed, enabled, disabled, and re-enabled', async ({
|
||||
browser
|
||||
}) => {
|
||||
test.setTimeout(45 * 60 * 1000)
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
const excluded = new Set([
|
||||
'discordbot',
|
||||
'usermanager',
|
||||
'lnurldevice',
|
||||
'scheduler',
|
||||
'deezy',
|
||||
'webpages'
|
||||
])
|
||||
const adminExtensions = new Set(['nostrclient'])
|
||||
const extensionIds = [...new Set([...state.extensionById.keys()])]
|
||||
.filter(extension => !excluded.has(extension))
|
||||
.sort()
|
||||
|
||||
expect(extensionIds.length).toBeGreaterThan(0)
|
||||
|
||||
for (const extension of extensionIds) {
|
||||
await uninstallExtension(state.adminContext, extension)
|
||||
await pageStatus(state.adminContext, `/${extension}`, [200, 403, 404])
|
||||
}
|
||||
|
||||
for (const extension of extensionIds) {
|
||||
await installLatestExtensionViaUi(admin, extension)
|
||||
await pageStatus(state.adminContext, `/${extension}`, [200, 403, 404])
|
||||
await enableExtensionViaUi(admin, extension)
|
||||
await pageStatus(
|
||||
state.adminContext,
|
||||
`/${extension}`,
|
||||
adminExtensions.has(extension) ? [200, 403] : 200
|
||||
)
|
||||
await disableExtensionViaUi(admin, extension)
|
||||
await pageStatus(state.adminContext, `/${extension}`, [200, 403, 404])
|
||||
await enableExtensionViaUi(admin, extension)
|
||||
await pageStatus(
|
||||
state.adminContext,
|
||||
`/${extension}`,
|
||||
adminExtensions.has(extension) ? [200, 403] : 200
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
apiKeyHeaders,
|
||||
clearMirror,
|
||||
createWallet,
|
||||
getPayment,
|
||||
jsonRequest,
|
||||
lnurlScan,
|
||||
pageStatus,
|
||||
pollPayment,
|
||||
pollWalletBalance,
|
||||
newUserWithUi,
|
||||
getAdminSession,
|
||||
payLnurlViaUi,
|
||||
withdrawLnurlViaUi,
|
||||
ensureExtensionsInstalledViaUi,
|
||||
enableExtensionViaUi,
|
||||
createPayLink,
|
||||
createWithdrawLink
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('002 lnurlp and withdraw scenarios', async ({browser}) => {
|
||||
test.setTimeout(5 * 60 * 1000)
|
||||
await clearMirror()
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
await ensureExtensionsInstalledViaUi(admin, ['lnurlp', 'withdraw'])
|
||||
const user = await newUserWithUi(browser, 'lnurl')
|
||||
await enableExtensionViaUi(user, 'lnurlp')
|
||||
await pageStatus(user.context, '/lnurlp/')
|
||||
|
||||
const initialPayLinks = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
'/lnurlp/api/v1/links',
|
||||
{
|
||||
headers: apiKeyHeaders(user.inkey),
|
||||
params: {all_wallets: true}
|
||||
}
|
||||
)
|
||||
expect(initialPayLinks).toEqual([])
|
||||
|
||||
let userBalanceSats = 0
|
||||
for (const index of [1, 2, 3]) {
|
||||
const payLink = await createPayLink(user.context, user, index)
|
||||
await pageStatus(user.context, `/lnurlp/link/${payLink.id}`)
|
||||
const fetched = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/lnurlp/api/v1/links/${payLink.id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(user.inkey),
|
||||
params: {all_wallets: true}
|
||||
}
|
||||
)
|
||||
expect(fetched.description).toBe(`receive payments ${index}`)
|
||||
|
||||
const lnurlResponse = await lnurlScan(
|
||||
user.context,
|
||||
fetched.lnurl,
|
||||
user.adminkey
|
||||
)
|
||||
expect(lnurlResponse.tag).toBe('payRequest')
|
||||
expect(lnurlResponse.commentAllowed).toBe(128)
|
||||
|
||||
for (let paymentIndex = 0; paymentIndex < 2; paymentIndex++) {
|
||||
const payment = await payLnurlViaUi(
|
||||
admin,
|
||||
fetched.lnurl,
|
||||
(10 + index) * 1000,
|
||||
`receive payments ${index}`
|
||||
)
|
||||
expect(payment.payment_hash).toBeTruthy()
|
||||
userBalanceSats += 10 + index
|
||||
await pollWalletBalance(user.context, user.inkey, userBalanceSats * 1000)
|
||||
const stored = await pollPayment(
|
||||
user.context,
|
||||
user.inkey,
|
||||
payment.payment_hash,
|
||||
result => Boolean(result?.details?.extra?.wh_response)
|
||||
)
|
||||
expect(stored.details.extra.wh_response).toContain('"b2":2')
|
||||
expect(stored.details.extra.wh_response).toContain(payment.payment_hash)
|
||||
}
|
||||
}
|
||||
|
||||
await enableExtensionViaUi(user, 'withdraw')
|
||||
await pageStatus(user.context, '/withdraw/')
|
||||
const initialWithdrawLinks = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
'/withdraw/api/v1/links',
|
||||
{
|
||||
headers: apiKeyHeaders(user.inkey),
|
||||
params: {all_wallets: true}
|
||||
}
|
||||
)
|
||||
expect(initialWithdrawLinks.data ?? initialWithdrawLinks).toEqual([])
|
||||
|
||||
const receiveWallet = await createWallet(user.context, 'receive wallet')
|
||||
|
||||
for (const index of [1, 2, 3]) {
|
||||
const withdrawLink = await createWithdrawLink(user.context, user, index)
|
||||
await pageStatus(user.context, `/withdraw/${withdrawLink.id}`)
|
||||
const fetched = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/withdraw/api/v1/links/${withdrawLink.id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(user.inkey),
|
||||
params: {all_wallets: true}
|
||||
}
|
||||
)
|
||||
expect(fetched.title).toBe(`withdraw ${index}`)
|
||||
const lnurlResponse = await lnurlScan(
|
||||
user.context,
|
||||
fetched.lnurl,
|
||||
user.adminkey
|
||||
)
|
||||
expect(lnurlResponse.tag).toBe('withdrawRequest')
|
||||
|
||||
for (let withdrawIndex = 0; withdrawIndex < 2; withdrawIndex++) {
|
||||
const withdrawal = await withdrawLnurlViaUi(
|
||||
user,
|
||||
fetched.lnurl,
|
||||
10,
|
||||
`withdraw ${index}`,
|
||||
receiveWallet.walletId
|
||||
)
|
||||
expect(withdrawal.payment_hash).toBeTruthy()
|
||||
userBalanceSats -= 10
|
||||
await pollWalletBalance(user.context, user.inkey, userBalanceSats * 1000)
|
||||
await getPayment(
|
||||
user.context,
|
||||
receiveWallet.inkey,
|
||||
withdrawal.payment_hash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await pollWalletBalance(user.context, receiveWallet.inkey, 60_000)
|
||||
})
|
||||
@@ -0,0 +1,173 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
state,
|
||||
apiKeyHeaders,
|
||||
createWallet,
|
||||
getWallet,
|
||||
jsonRequest,
|
||||
pageStatus,
|
||||
pollWalletBalance,
|
||||
newUserWithUi,
|
||||
getAdminSession,
|
||||
payInvoiceViaUi,
|
||||
ensureExtensionsInstalledViaUi,
|
||||
enableExtensionViaUi,
|
||||
withdrawLnurl
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('003 tpos payments, tips, and ATM withdraw', async ({browser}) => {
|
||||
test.setTimeout(5 * 60 * 1000)
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
await ensureExtensionsInstalledViaUi(admin, ['tpos'])
|
||||
const user = await newUserWithUi(browser, 'tpos')
|
||||
await enableExtensionViaUi(user, 'tpos')
|
||||
await pageStatus(user.context, '/tpos/')
|
||||
|
||||
const tposs = await jsonRequest(user.context, 'get', '/tpos/api/v1/tposs', {
|
||||
headers: apiKeyHeaders(user.inkey),
|
||||
params: {all_wallets: true}
|
||||
})
|
||||
expect(tposs).toEqual([])
|
||||
|
||||
const tipsWallet = await createWallet(user.context, 'tips wallet')
|
||||
expect(tipsWallet.userId).toBe(user.userId)
|
||||
|
||||
const tpos = await jsonRequest(user.context, 'post', '/tpos/api/v1/tposs', {
|
||||
headers: apiKeyHeaders(user.adminkey),
|
||||
data: {name: 'Test', wallet: user.walletId, currency: 'USD'},
|
||||
expected: 201
|
||||
})
|
||||
expect(tpos.wallet).toBe(user.walletId)
|
||||
|
||||
const updated = await jsonRequest(
|
||||
user.context,
|
||||
'put',
|
||||
`/tpos/api/v1/tposs/${tpos.id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(user.adminkey),
|
||||
data: {
|
||||
name: 'Test',
|
||||
wallet: user.walletId,
|
||||
currency: 'USD',
|
||||
tip_options: '[2, 5]',
|
||||
tip_wallet: tipsWallet.walletId
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(updated.tip_wallet).toBe(tipsWallet.walletId)
|
||||
|
||||
const atm = await jsonRequest(user.context, 'post', '/tpos/api/v1/tposs', {
|
||||
headers: apiKeyHeaders(user.adminkey),
|
||||
data: {
|
||||
withdraw_pin: 2222,
|
||||
withdraw_between: 1,
|
||||
name: 'Test ATM',
|
||||
wallet: user.walletId,
|
||||
currency: 'EUR',
|
||||
withdraw_limit: 100000
|
||||
},
|
||||
expected: 201
|
||||
})
|
||||
expect(atm.wallet).toBe(user.walletId)
|
||||
|
||||
await pageStatus(user.context, `/tpos/${tpos.id}`)
|
||||
await jsonRequest(user.context, 'get', '/api/v1/rate/EUR')
|
||||
|
||||
let expectedMerchantBalance = 0
|
||||
for (const amount of [21, 42]) {
|
||||
const invoice = await jsonRequest(
|
||||
user.context,
|
||||
'post',
|
||||
`/tpos/api/v1/tposs/${tpos.id}/invoices`,
|
||||
{
|
||||
headers: apiKeyHeaders(user.inkey),
|
||||
data: {
|
||||
amount,
|
||||
memo: 'TPoS test',
|
||||
...(amount === 42 ? {tipAmount: 3} : {})
|
||||
},
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(invoice.bolt11).toBeTruthy()
|
||||
await payInvoiceViaUi(admin, invoice.bolt11)
|
||||
await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/tpos/api/v1/tposs/${tpos.id}/invoices/${invoice.payment_hash}`,
|
||||
{headers: apiKeyHeaders(user.inkey)}
|
||||
)
|
||||
expectedMerchantBalance += amount
|
||||
await pollWalletBalance(
|
||||
user.context,
|
||||
user.inkey,
|
||||
expectedMerchantBalance * 1000
|
||||
)
|
||||
}
|
||||
|
||||
await pageStatus(user.context, `/tpos/${atm.id}`)
|
||||
await jsonRequest(user.context, 'get', '/api/v1/rate/EUR')
|
||||
const atmInvoice = await jsonRequest(
|
||||
user.context,
|
||||
'post',
|
||||
`/tpos/api/v1/tposs/${atm.id}/invoices`,
|
||||
{
|
||||
headers: apiKeyHeaders(user.inkey),
|
||||
data: {amount: 11000, memo: 'TPoS test'},
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
await payInvoiceViaUi(admin, atmInvoice.bolt11)
|
||||
|
||||
const beforeAdmin = await getWallet(
|
||||
state.adminContext,
|
||||
state.adminWallet.inkey
|
||||
)
|
||||
const beforeUser = await getWallet(user.context, user.inkey)
|
||||
const atmPin = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/tpos/api/v1/atm/${atm.id}/2222`,
|
||||
{
|
||||
headers: apiKeyHeaders(state.adminWallet.adminkey)
|
||||
}
|
||||
)
|
||||
expect(atmPin.id).toBeTruthy()
|
||||
const withdraw = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/tpos/api/v1/atm/withdraw/${atmPin.id}/100`,
|
||||
{
|
||||
headers: apiKeyHeaders(state.adminWallet.adminkey)
|
||||
}
|
||||
)
|
||||
const withdrawResponse = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/tpos/api/v1/lnurl/${withdraw.id}/100`,
|
||||
{headers: apiKeyHeaders(state.adminWallet.adminkey)}
|
||||
)
|
||||
const payment = await withdrawLnurl(
|
||||
user.context,
|
||||
state.adminWallet,
|
||||
withdrawResponse,
|
||||
100,
|
||||
'TPoS withdraw'
|
||||
)
|
||||
expect(payment.payment_hash).toBeTruthy()
|
||||
await pollWalletBalance(
|
||||
state.adminContext,
|
||||
state.adminWallet.inkey,
|
||||
beforeAdmin.balance + 100_000
|
||||
)
|
||||
await pollWalletBalance(
|
||||
user.context,
|
||||
user.inkey,
|
||||
beforeUser.balance - 100_000
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
state,
|
||||
jsonRequest,
|
||||
latestRelease,
|
||||
pageStatus,
|
||||
topUpWallet,
|
||||
newUserWithUi,
|
||||
getAdminSession,
|
||||
ensureExtensionsInstalledViaUi,
|
||||
enableExtensionViaUi,
|
||||
disableExtensionViaUi,
|
||||
payToEnableExtensionViaUi
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('004 tpos pay-to-enable flow', async ({browser}) => {
|
||||
test.setTimeout(5 * 60 * 1000)
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
await ensureExtensionsInstalledViaUi(admin, ['tpos'])
|
||||
const user = await newUserWithUi(browser, 'pay-to-enable')
|
||||
await topUpWallet(state.adminContext, user.walletId, 1000)
|
||||
|
||||
const release = await latestRelease(state.adminContext, 'tpos')
|
||||
expect(release.version).toBeTruthy()
|
||||
await enableExtensionViaUi(admin, 'tpos')
|
||||
await jsonRequest(state.adminContext, 'put', '/api/v1/extension/tpos/sell', {
|
||||
data: {required: true, amount: 21, wallet: state.adminWallet.walletId}
|
||||
})
|
||||
|
||||
await pageStatus(state.adminContext, '/tpos')
|
||||
|
||||
await pageStatus(user.context, '/tpos', [200, 402, 403])
|
||||
const lowInvoice = await jsonRequest(
|
||||
user.context,
|
||||
'put',
|
||||
'/api/v1/extension/tpos/invoice/enable',
|
||||
{data: {amount: 1}, expected: 400}
|
||||
)
|
||||
expect(JSON.stringify(lowInvoice)).toContain('21')
|
||||
|
||||
await jsonRequest(user.context, 'put', '/api/v1/extension/tpos/enable', {
|
||||
expected: 402
|
||||
})
|
||||
await payToEnableExtensionViaUi(user, 'tpos')
|
||||
await pageStatus(user.context, '/tpos')
|
||||
await disableExtensionViaUi(user, 'tpos')
|
||||
await enableExtensionViaUi(user, 'tpos')
|
||||
await pageStatus(user.context, '/tpos')
|
||||
|
||||
await jsonRequest(state.adminContext, 'put', '/api/v1/extension/tpos/sell', {
|
||||
data: {required: false, amount: 0, wallet: state.adminWallet.walletId}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
createWallet,
|
||||
getPayments,
|
||||
jsonRequest,
|
||||
newUserWithUi,
|
||||
getAdminSession,
|
||||
payLnurlViaUi,
|
||||
ensureExtensionsInstalledViaUi,
|
||||
enableExtensionViaUi,
|
||||
createPayLink,
|
||||
createWithdrawLink,
|
||||
withdrawLnurl
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('005 lnurlw race limits successful withdrawals', async ({browser}) => {
|
||||
test.setTimeout(5 * 60 * 1000)
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
await ensureExtensionsInstalledViaUi(admin, ['lnurlp', 'withdraw'])
|
||||
const user = await newUserWithUi(browser, 'lnurl-race')
|
||||
await enableExtensionViaUi(user, 'lnurlp')
|
||||
await enableExtensionViaUi(user, 'withdraw')
|
||||
|
||||
const payLink = await createPayLink(user.context, user, 1, {
|
||||
description: 'receive payments',
|
||||
min: 1,
|
||||
max: 100_000_000,
|
||||
webhook_url: undefined,
|
||||
webhook_headers: undefined,
|
||||
webhook_body: undefined
|
||||
})
|
||||
const payResponse = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/lnurlp/${payLink.id}`
|
||||
)
|
||||
expect(payResponse.tag).toBe('payRequest')
|
||||
await payLnurlViaUi(admin, payLink.lnurl, 10_000_000, 'receive payments')
|
||||
|
||||
const receiveWallet = await createWallet(user.context, 'race receive wallet')
|
||||
const withdrawLink = await createWithdrawLink(user.context, user, 1, {
|
||||
uses: 2,
|
||||
max_withdrawable: 10
|
||||
})
|
||||
const withdrawResponse = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/withdraw/api/v1/lnurl/${withdrawLink.unique_hash}`
|
||||
)
|
||||
|
||||
// The race assertion is intentionally API-driven: it needs concurrent calls,
|
||||
// not serialized browser clicks.
|
||||
const attempts = Array.from({length: 100}, () =>
|
||||
withdrawLnurl(
|
||||
user.context,
|
||||
receiveWallet,
|
||||
withdrawResponse,
|
||||
10,
|
||||
'withdraw 1'
|
||||
)
|
||||
)
|
||||
await Promise.all(attempts)
|
||||
|
||||
const payments = await getPayments(user.context, receiveWallet.inkey)
|
||||
expect(payments).toHaveLength(100)
|
||||
const successCount = payments.filter(
|
||||
payment => payment.status === 'success'
|
||||
).length
|
||||
expect(successCount).toBe(2)
|
||||
})
|
||||
@@ -0,0 +1,224 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
apiKeyHeaders,
|
||||
clearMirror,
|
||||
getPayments,
|
||||
jsonRequest,
|
||||
mirrorUrl,
|
||||
pageStatus,
|
||||
pollWalletBalance,
|
||||
newUserWithUi,
|
||||
getAdminSession,
|
||||
payInvoiceViaUi,
|
||||
ensureExtensionsInstalledViaUi,
|
||||
enableExtensionViaUi
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('006 watchonly, satspay, and tipjar scenario', async ({browser}) => {
|
||||
test.setTimeout(7 * 60 * 1000)
|
||||
await clearMirror()
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
await ensureExtensionsInstalledViaUi(admin, [
|
||||
'watchonly',
|
||||
'satspay',
|
||||
'tipjar'
|
||||
])
|
||||
const user = await newUserWithUi(browser, 'watchonly-satspay-tipjar')
|
||||
await enableExtensionViaUi(user, 'watchonly')
|
||||
await pageStatus(user.context, `/watchonly/?usr=${user.userId}`)
|
||||
await jsonRequest(user.context, 'get', '/watchonly/api/v1/config', {
|
||||
headers: apiKeyHeaders(user.inkey)
|
||||
})
|
||||
await jsonRequest(user.context, 'get', '/watchonly/api/v1/wallet', {
|
||||
headers: apiKeyHeaders(user.inkey),
|
||||
params: {network: 'Mainnet'}
|
||||
})
|
||||
const onchain = await jsonRequest(
|
||||
user.context,
|
||||
'post',
|
||||
'/watchonly/api/v1/wallet',
|
||||
{
|
||||
headers: apiKeyHeaders(user.adminkey),
|
||||
data: {
|
||||
title: 'segwit',
|
||||
masterpub:
|
||||
'zpub6rsRjqj6BTbD9DjqrY4p14tUx5kdA8ZGCTJD99wZTxD5wfvCkyXKrK3s7M3B1eFN6NbRhmbDDRDC8LF3Bn5gmxxN9rF8mDpZsGC6isGrK1g',
|
||||
network: 'Mainnet',
|
||||
meta: '{"accountPath":"m/84\'/0\'/0\'"}'
|
||||
},
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(onchain.id).toBeTruthy()
|
||||
await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/watchonly/api/v1/addresses/${onchain.id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(user.inkey)
|
||||
}
|
||||
)
|
||||
await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/watchonly/api/v1/address/${onchain.id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(user.inkey)
|
||||
}
|
||||
)
|
||||
|
||||
await enableExtensionViaUi(user, 'satspay')
|
||||
await pageStatus(user.context, '/satspay/')
|
||||
await jsonRequest(user.context, 'get', '/satspay/api/v1/charges', {
|
||||
headers: apiKeyHeaders(user.inkey)
|
||||
})
|
||||
const onchainCharge = await jsonRequest(
|
||||
user.context,
|
||||
'post',
|
||||
'/satspay/api/v1/charge',
|
||||
{
|
||||
headers: apiKeyHeaders(user.adminkey),
|
||||
data: {
|
||||
onchain: true,
|
||||
onchainwallet: onchain.id,
|
||||
lnbits: false,
|
||||
description: 'Onchain Charge',
|
||||
time: 1111,
|
||||
amount: 1111,
|
||||
lnbitswallet: null
|
||||
},
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(onchainCharge.id).toBeTruthy()
|
||||
|
||||
await jsonRequest(user.context, 'post', '/satspay/api/v1/charge', {
|
||||
headers: apiKeyHeaders(user.adminkey),
|
||||
data: {
|
||||
onchain: false,
|
||||
onchainwallet: null,
|
||||
lnbits: true,
|
||||
description: 'lightning charge - to expire',
|
||||
time: 1,
|
||||
amount: 10,
|
||||
lnbitswallet: user.walletId,
|
||||
webhook: 'https://google.com',
|
||||
completelink: 'https://twitter.com',
|
||||
completelinktext: 'Have Fun'
|
||||
},
|
||||
expected: 201
|
||||
})
|
||||
|
||||
let expectedBalanceSats = 0
|
||||
for (let index = 1; index <= 5; index++) {
|
||||
const amount = 9 + index
|
||||
const charge = await jsonRequest(
|
||||
user.context,
|
||||
'post',
|
||||
'/satspay/api/v1/charge',
|
||||
{
|
||||
headers: apiKeyHeaders(user.adminkey),
|
||||
data: {
|
||||
onchain: false,
|
||||
onchainwallet: null,
|
||||
lnbits: true,
|
||||
description: `lightning charge [${index}]`,
|
||||
time: 1220,
|
||||
amount,
|
||||
lnbitswallet: user.walletId,
|
||||
webhook: mirrorUrl(),
|
||||
completelink: 'https://twitter.com',
|
||||
completelinktext: 'Have Fun'
|
||||
},
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(charge.payment_request).toBeTruthy()
|
||||
await pageStatus(user.context, `/satspay/${charge.id}`)
|
||||
await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/satspay/api/v1/charge/balance/${charge.id}`
|
||||
)
|
||||
await payInvoiceViaUi(admin, charge.payment_request)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const updatedCharge = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/satspay/api/v1/charge/${charge.id}`
|
||||
)
|
||||
return Boolean(updatedCharge.paid || updatedCharge.lnbitswallet)
|
||||
})
|
||||
.toBeTruthy()
|
||||
expectedBalanceSats += amount
|
||||
}
|
||||
|
||||
await pollWalletBalance(user.context, user.inkey, expectedBalanceSats * 1000)
|
||||
const charges = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
'/satspay/api/v1/charges',
|
||||
{
|
||||
headers: apiKeyHeaders(user.inkey)
|
||||
}
|
||||
)
|
||||
expect(charges.length).toBeGreaterThanOrEqual(7)
|
||||
const payments = await getPayments(user.context, user.inkey)
|
||||
expect(payments.length).toBeGreaterThanOrEqual(6)
|
||||
|
||||
await enableExtensionViaUi(user, 'tipjar')
|
||||
await pageStatus(user.context, `/tipjar/?usr=${user.userId}`)
|
||||
await jsonRequest(user.context, 'get', '/tipjar/api/v1/tipjars', {
|
||||
headers: apiKeyHeaders(user.inkey)
|
||||
})
|
||||
const tipjar = await jsonRequest(
|
||||
user.context,
|
||||
'post',
|
||||
'/tipjar/api/v1/tipjars',
|
||||
{
|
||||
headers: apiKeyHeaders(user.adminkey),
|
||||
data: {wallet: user.walletId, name: 'Nakamoto', webhook: mirrorUrl()},
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(tipjar.id).toBeTruthy()
|
||||
await pageStatus(user.context, `/tipjar/${tipjar.id}`)
|
||||
|
||||
for (let index = 1; index <= 5; index++) {
|
||||
const tip = await jsonRequest(user.context, 'post', '/tipjar/api/v1/tips', {
|
||||
headers: apiKeyHeaders(user.adminkey),
|
||||
data: {
|
||||
tipjar: tipjar.id,
|
||||
name: 'Hal',
|
||||
sats: 21,
|
||||
message: `Let's go ...${index}!`
|
||||
},
|
||||
expected: 201
|
||||
})
|
||||
expect(tip.redirect_url).toBeTruthy()
|
||||
const tipChargeId = tip.redirect_url.split('/').filter(Boolean).at(-1)
|
||||
const charge = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
`/satspay/api/v1/charge/${tipChargeId}`
|
||||
)
|
||||
expect(charge.description).toBe(`Let's go ...${index}!`)
|
||||
const tips = await jsonRequest(user.context, 'get', '/tipjar/api/v1/tips', {
|
||||
headers: apiKeyHeaders(user.inkey)
|
||||
})
|
||||
expect(tips.length).toBe(index)
|
||||
await payInvoiceViaUi(admin, tip.payment_request || charge.payment_request)
|
||||
expectedBalanceSats += 21
|
||||
await pollWalletBalance(
|
||||
user.context,
|
||||
user.inkey,
|
||||
expectedBalanceSats * 1000
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,134 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
jsonRequest,
|
||||
pageStatus,
|
||||
newUserWithUi,
|
||||
getAdminSession,
|
||||
createInvoiceViaUi,
|
||||
payInvoiceViaUi,
|
||||
ensureExtensionsInstalledViaUi,
|
||||
enableExtensionViaUi,
|
||||
lndhubHeaders
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('007 lndhub mobile wallet API scenario', async ({browser}) => {
|
||||
test.setTimeout(5 * 60 * 1000)
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
await ensureExtensionsInstalledViaUi(admin, ['lndhub'])
|
||||
const user = await newUserWithUi(browser, 'lndhub')
|
||||
await enableExtensionViaUi(user, 'lndhub')
|
||||
await pageStatus(user.context, '/lndhub/')
|
||||
await jsonRequest(user.context, 'post', '/api/v1/auth/logout')
|
||||
await jsonRequest(user.context, 'get', '/lndhub/ext/getinfo')
|
||||
|
||||
for (const index of [1, 2, 3]) {
|
||||
const userInvoice = await createInvoiceViaUi(
|
||||
user,
|
||||
21,
|
||||
`user invoice ${index}`
|
||||
)
|
||||
await payInvoiceViaUi(admin, userInvoice.bolt11)
|
||||
const adminInvoice = await createInvoiceViaUi(
|
||||
admin,
|
||||
1,
|
||||
`admin invoice ${index}`
|
||||
)
|
||||
await payInvoiceViaUi(user, adminInvoice.bolt11)
|
||||
}
|
||||
|
||||
const auth = await jsonRequest(user.context, 'post', '/lndhub/ext/auth', {
|
||||
data: {login: 'some_user', password: user.adminkey, refresh_token: ''}
|
||||
})
|
||||
expect(auth.access_token).toBeTruthy()
|
||||
const authHeaders = await lndhubHeaders(auth.access_token)
|
||||
|
||||
const balance = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
'/lndhub/ext/balance',
|
||||
{
|
||||
headers: authHeaders
|
||||
}
|
||||
)
|
||||
expect(balance.BTC.AvailableBalance).toBe(60)
|
||||
const txs = await jsonRequest(user.context, 'get', '/lndhub/ext/gettxs', {
|
||||
headers: authHeaders
|
||||
})
|
||||
expect(txs).toHaveLength(3)
|
||||
expect(txs.every(tx => tx.value === -1)).toBeTruthy()
|
||||
const invoices = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
'/lndhub/ext/getuserinvoices',
|
||||
{
|
||||
headers: authHeaders
|
||||
}
|
||||
)
|
||||
expect(invoices).toHaveLength(3)
|
||||
expect(invoices.every(invoice => invoice.amt === 21)).toBeTruthy()
|
||||
|
||||
const mobileInvoice = await jsonRequest(
|
||||
user.context,
|
||||
'post',
|
||||
'/lndhub/ext/addinvoice',
|
||||
{
|
||||
headers: authHeaders,
|
||||
data: {amt: 50, memo: '50 sats'}
|
||||
}
|
||||
)
|
||||
await payInvoiceViaUi(admin, mobileInvoice.payment_request)
|
||||
const invoicesAfter = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
'/lndhub/ext/getuserinvoices',
|
||||
{
|
||||
headers: authHeaders
|
||||
}
|
||||
)
|
||||
expect(invoicesAfter).toHaveLength(4)
|
||||
|
||||
const adminInvoice = await createInvoiceViaUi(admin, 30, '30 sats')
|
||||
const paid = await jsonRequest(
|
||||
user.context,
|
||||
'post',
|
||||
'/lndhub/ext/payinvoice',
|
||||
{
|
||||
headers: authHeaders,
|
||||
data: {invoice: adminInvoice.bolt11}
|
||||
}
|
||||
)
|
||||
expect(JSON.stringify(paid)).toContain('30 sats')
|
||||
const txsAfter = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
'/lndhub/ext/gettxs',
|
||||
{
|
||||
headers: authHeaders
|
||||
}
|
||||
)
|
||||
expect(txsAfter).toHaveLength(4)
|
||||
const balanceAfter = await jsonRequest(
|
||||
user.context,
|
||||
'get',
|
||||
'/lndhub/ext/balance',
|
||||
{
|
||||
headers: authHeaders
|
||||
}
|
||||
)
|
||||
expect(balanceAfter.BTC.AvailableBalance).toBe(80)
|
||||
|
||||
for (const [method, path, data] of [
|
||||
['get', '/lndhub/ext/balance'],
|
||||
['get', '/lndhub/ext/gettxs'],
|
||||
['get', '/lndhub/ext/getuserinvoices'],
|
||||
['post', '/lndhub/ext/addinvoice', {amt: 50, memo: '50 sats'}],
|
||||
['post', '/lndhub/ext/payinvoice', {invoice: adminInvoice.bolt11}]
|
||||
]) {
|
||||
await jsonRequest(user.context, method, path, {data, expected: 400})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
state,
|
||||
jsonRequest,
|
||||
uninstallExtension,
|
||||
getAdminSession,
|
||||
refreshExtensionCatalog,
|
||||
installLatestExtensionViaUi,
|
||||
setExtensionActiveViaUi
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('008 lnaddress and lnurlp redirect conflict handling', async ({
|
||||
browser
|
||||
}) => {
|
||||
test.setTimeout(10 * 60 * 1000)
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
await jsonRequest(state.adminContext, 'patch', '/admin/api/v1/settings', {
|
||||
data: {
|
||||
lnbits_extensions_manifests: [
|
||||
'https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json',
|
||||
'https://raw.githubusercontent.com/lnbits/lnaddress/refs/heads/main/manifest.json'
|
||||
]
|
||||
}
|
||||
})
|
||||
await refreshExtensionCatalog(state.adminContext)
|
||||
await uninstallExtension(state.adminContext, 'lnaddress')
|
||||
|
||||
await installLatestExtensionViaUi(admin, 'lnurlp')
|
||||
await installLatestExtensionViaUi(admin, 'lnaddress')
|
||||
await setExtensionActiveViaUi(admin, 'lnurlp', false)
|
||||
const conflict = await setExtensionActiveViaUi(admin, 'lnurlp', true, 400)
|
||||
expect(JSON.stringify(conflict)).toContain('Already mapped')
|
||||
await setExtensionActiveViaUi(admin, 'lnaddress', false)
|
||||
await setExtensionActiveViaUi(admin, 'lnurlp', true)
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
state,
|
||||
jsonRequest,
|
||||
textRequest,
|
||||
getAdminSession,
|
||||
installExtensionVersionViaUi,
|
||||
enableExtensionViaUi
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('009 example extension can downgrade and upgrade data shape', async ({
|
||||
browser
|
||||
}) => {
|
||||
test.setTimeout(10 * 60 * 1000)
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
async function installAndCheck(testVersion, extensionVersion) {
|
||||
const releases = await jsonRequest(
|
||||
state.adminContext,
|
||||
'get',
|
||||
'/api/v1/extension/example/releases'
|
||||
)
|
||||
const release = releases.find(item => item.version === extensionVersion)
|
||||
expect(release).toBeTruthy()
|
||||
await installExtensionVersionViaUi(admin, 'example', release.version)
|
||||
await enableExtensionViaUi(admin, 'example')
|
||||
const page = await textRequest(state.adminContext, 'get', '/example')
|
||||
expect(page).toContain(
|
||||
`Do not remove. Test install extension version: ${testVersion}`
|
||||
)
|
||||
const response = await jsonRequest(
|
||||
state.adminContext,
|
||||
'get',
|
||||
'/example/api/v1/test/00000000'
|
||||
)
|
||||
expect(response.version).toBe(String(testVersion))
|
||||
expect(response.test_id).toBe('00000000')
|
||||
}
|
||||
|
||||
await installAndCheck(1, '1.0.1')
|
||||
await installAndCheck(2, '1.0.6')
|
||||
await installAndCheck(1, '1.0.1')
|
||||
await installAndCheck(2, '1.0.6')
|
||||
await installAndCheck(1, '1.0.1')
|
||||
})
|
||||
@@ -0,0 +1,375 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
state,
|
||||
apiKeyHeaders,
|
||||
createWallet,
|
||||
decodePayment,
|
||||
getWallet,
|
||||
jsonRequest,
|
||||
textRequest,
|
||||
newContext,
|
||||
newUserWithUi,
|
||||
getAdminSession,
|
||||
payInvoiceViaUi,
|
||||
ensureExtensionsInstalledViaUi,
|
||||
enableExtensionViaUi,
|
||||
createNip5Domain,
|
||||
createNip5Address,
|
||||
activateNip5Address,
|
||||
getNostrJson,
|
||||
buyNip5Address
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('010 nostrnip5 domain, search, pricing, and referral flow', async ({
|
||||
browser
|
||||
}) => {
|
||||
test.setTimeout(10 * 60 * 1000)
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
await ensureExtensionsInstalledViaUi(admin, ['nostrnip5', 'lnurlp'])
|
||||
const anonymous = await newContext()
|
||||
await textRequest(anonymous, 'patch', '/nostrnip5/api/v1/domain/ranking/0', {
|
||||
data: 'reserved_a\r\nreserved_b',
|
||||
headers: {'content-type': 'text/plain'},
|
||||
expected: 401
|
||||
})
|
||||
await enableExtensionViaUi(admin, 'nostrnip5')
|
||||
await enableExtensionViaUi(admin, 'lnurlp')
|
||||
await textRequest(
|
||||
state.adminContext,
|
||||
'patch',
|
||||
'/nostrnip5/api/v1/domain/ranking/0',
|
||||
{
|
||||
data: 'reserved_a\r\nreserved_b',
|
||||
headers: {'content-type': 'text/plain'}
|
||||
}
|
||||
)
|
||||
await textRequest(
|
||||
state.adminContext,
|
||||
'patch',
|
||||
'/nostrnip5/api/v1/domain/ranking/200',
|
||||
{
|
||||
data: 'rank_200_a\r\nrank_200_b\r\nyyy',
|
||||
headers: {'content-type': 'text/plain'}
|
||||
}
|
||||
)
|
||||
await textRequest(
|
||||
state.adminContext,
|
||||
'patch',
|
||||
'/nostrnip5/api/v1/domain/ranking/1000',
|
||||
{
|
||||
data: 'rank_1000_a\r\nrank_1000_b\r\nrank_1000_c\r\nxxx',
|
||||
headers: {'content-type': 'text/plain'}
|
||||
}
|
||||
)
|
||||
await jsonRequest(state.adminContext, 'put', '/nostrnip5/api/v1/settings', {
|
||||
headers: apiKeyHeaders(state.adminWallet.adminkey),
|
||||
data: {
|
||||
lnaddress_api_endpoint: state.baseURL,
|
||||
lnaddress_api_admin_key: state.adminWallet.adminkey
|
||||
}
|
||||
})
|
||||
|
||||
const owner = await newUserWithUi(browser, 'nostrnip5-owner')
|
||||
await enableExtensionViaUi(owner, 'nostrnip5')
|
||||
await enableExtensionViaUi(owner, 'lnurlp')
|
||||
await textRequest(
|
||||
owner.context,
|
||||
'patch',
|
||||
'/nostrnip5/api/v1/domain/ranking/0',
|
||||
{
|
||||
data: 'reserved_a\r\nreserved_b',
|
||||
headers: {'content-type': 'text/plain'},
|
||||
expected: 403
|
||||
}
|
||||
)
|
||||
|
||||
const domain = await createNip5Domain(owner.context, owner)
|
||||
const domainId = domain.id
|
||||
await jsonRequest(
|
||||
owner.context,
|
||||
'get',
|
||||
`/nostrnip5/api/v1/domain/${domainId}`,
|
||||
{
|
||||
headers: apiKeyHeaders(owner.inkey)
|
||||
}
|
||||
)
|
||||
|
||||
await jsonRequest(owner.context, 'put', '/nostrnip5/api/v1/domain', {
|
||||
headers: apiKeyHeaders(owner.adminkey),
|
||||
data: {
|
||||
cost: 100,
|
||||
cost_extra: {
|
||||
char_count_cost: [
|
||||
{bracket: '1', amount: '1001'},
|
||||
{bracket: '2', amount: '505'},
|
||||
{bracket: '3', amount: '202'}
|
||||
],
|
||||
max_years: '5',
|
||||
promotions: [
|
||||
{
|
||||
code: 'code_10',
|
||||
buyer_discount_percent: '10',
|
||||
referer_bonus_percent: '0'
|
||||
},
|
||||
{
|
||||
code: 'code_20',
|
||||
buyer_discount_percent: '20',
|
||||
referer_bonus_percent: '5'
|
||||
},
|
||||
{
|
||||
code: 'code_alan',
|
||||
buyer_discount_percent: '25',
|
||||
referer_bonus_percent: '20',
|
||||
selected_referer: 'alan'
|
||||
}
|
||||
],
|
||||
rank_cost: [
|
||||
{bracket: 200, amount: '10000'},
|
||||
{bracket: 1000, amount: '500'}
|
||||
]
|
||||
},
|
||||
currency: 'USD',
|
||||
domain: domain.domain,
|
||||
id: domainId,
|
||||
wallet: owner.walletId
|
||||
}
|
||||
})
|
||||
|
||||
const identifierOne = `one_${Date.now()}`
|
||||
const identifierTwo = `two_${Date.now()}`
|
||||
const identifierThree = `three_${Date.now()}`
|
||||
const address = await createNip5Address(
|
||||
owner.context,
|
||||
owner,
|
||||
domainId,
|
||||
identifierOne,
|
||||
'04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9'
|
||||
)
|
||||
await getNostrJson(owner.context, domainId, identifierOne, 404)
|
||||
await activateNip5Address(owner.context, owner, domainId, address.id)
|
||||
let nostrJson = await getNostrJson(owner.context, domainId, identifierOne)
|
||||
expect(nostrJson.names[identifierOne]).toBeTruthy()
|
||||
|
||||
await jsonRequest(
|
||||
owner.context,
|
||||
'put',
|
||||
`/nostrnip5/api/v1/domain/${domainId}/address/${address.id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(owner.adminkey),
|
||||
data: {
|
||||
pubkey:
|
||||
'04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbeeee',
|
||||
relays: ['wss://relay.nostr.com', 'ws://relay.nostr.org']
|
||||
}
|
||||
}
|
||||
)
|
||||
nostrJson = await getNostrJson(owner.context, domainId, identifierOne)
|
||||
expect(nostrJson.relays).toBeTruthy()
|
||||
|
||||
const second = await createNip5Address(
|
||||
owner.context,
|
||||
owner,
|
||||
domainId,
|
||||
identifierTwo,
|
||||
'04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfaaaaa'
|
||||
)
|
||||
await activateNip5Address(owner.context, owner, domainId, second.id)
|
||||
let addresses = await jsonRequest(
|
||||
owner.context,
|
||||
'get',
|
||||
'/nostrnip5/api/v1/addresses/paginated',
|
||||
{
|
||||
headers: apiKeyHeaders(owner.inkey),
|
||||
params: {
|
||||
all_wallets: true,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
sortby: 'time',
|
||||
direction: 'asc'
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(JSON.stringify(addresses)).toContain(identifierTwo)
|
||||
|
||||
await jsonRequest(
|
||||
owner.context,
|
||||
'delete',
|
||||
`/nostrnip5/api/v1/domain/${domainId}/address/${address.id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(owner.adminkey)
|
||||
}
|
||||
)
|
||||
await getNostrJson(owner.context, domainId, identifierOne, 404)
|
||||
|
||||
const alan = await createNip5Address(
|
||||
owner.context,
|
||||
owner,
|
||||
domainId,
|
||||
'alan',
|
||||
'04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfaaabb'
|
||||
)
|
||||
await activateNip5Address(owner.context, owner, domainId, alan.id)
|
||||
const alanWallet = await createWallet(owner.context, 'Alan Wallet')
|
||||
await jsonRequest(
|
||||
owner.context,
|
||||
'put',
|
||||
`/nostrnip5/api/v1/user/domain/${domainId}/address/${alan.id}/lnaddress`,
|
||||
{
|
||||
headers: apiKeyHeaders(owner.adminkey),
|
||||
data: {wallet: alanWallet.walletId, min: 5, max: '100005'}
|
||||
}
|
||||
)
|
||||
|
||||
for (const [query, expectedAvailable] of [
|
||||
['a', true],
|
||||
['aa', true],
|
||||
['aaa', true],
|
||||
['aaaa', true],
|
||||
[identifierTwo, false],
|
||||
['reserved_a', false],
|
||||
['rank_200_b', true],
|
||||
['rank_1000_b', true],
|
||||
['yyy', true],
|
||||
['xxx', true]
|
||||
]) {
|
||||
const result = await jsonRequest(
|
||||
owner.context,
|
||||
'get',
|
||||
`/nostrnip5/api/v1/domain/${domainId}/search`,
|
||||
{
|
||||
headers: apiKeyHeaders(owner.inkey),
|
||||
params: {q: query}
|
||||
}
|
||||
)
|
||||
expect(Boolean(result.available)).toBe(expectedAvailable)
|
||||
}
|
||||
|
||||
const client = await newUserWithUi(browser, 'nostrnip5-client')
|
||||
await enableExtensionViaUi(client, 'nostrnip5')
|
||||
await enableExtensionViaUi(client, 'lnurlp')
|
||||
|
||||
await buyNip5Address(client.context, client, domainId, 'abc1', {
|
||||
pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfaaaaa',
|
||||
years: 3
|
||||
})
|
||||
await buyNip5Address(client.context, client, domainId, 'a1', {
|
||||
pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfaaaaa',
|
||||
years: 3
|
||||
})
|
||||
const cartItems = await jsonRequest(
|
||||
client.context,
|
||||
'get',
|
||||
'/nostrnip5/api/v1/user/addresses',
|
||||
{
|
||||
headers: apiKeyHeaders(client.inkey),
|
||||
params: {active: false}
|
||||
}
|
||||
)
|
||||
expect(JSON.stringify(cartItems)).toContain('abc1')
|
||||
expect(JSON.stringify(cartItems)).toContain('a1')
|
||||
|
||||
const priceChecks = [
|
||||
['abc1', 3, null, false, 300.0],
|
||||
['abc1', 5, null, true, 500.0],
|
||||
['abc1', 7, null, false, null, 400],
|
||||
['abc1', 3, 'code_10', true, 270.0],
|
||||
['abc1', 3, 'code_20', false, 240.0],
|
||||
['abc1', 3, 'code_alan', true, 225.0],
|
||||
['xyz', 3, null, false, 606.0],
|
||||
['xyz', 5, null, true, 1010.0],
|
||||
['xyz', 7, null, false, null, 400],
|
||||
['xyz', 3, 'code_10', true, 545.4],
|
||||
['xyz', 3, 'code_20', false, 484.8],
|
||||
['xyz', 3, 'code_alan', true, 454.5]
|
||||
]
|
||||
|
||||
for (const [
|
||||
localPart,
|
||||
years,
|
||||
promoCode,
|
||||
createInvoiceFlag,
|
||||
expectedPrice,
|
||||
expectedStatus
|
||||
] of priceChecks) {
|
||||
const result = await buyNip5Address(
|
||||
client.context,
|
||||
client,
|
||||
domainId,
|
||||
localPart,
|
||||
{
|
||||
pubkey:
|
||||
'04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfaaaaa',
|
||||
years,
|
||||
promo_code: promoCode,
|
||||
create_invoice: createInvoiceFlag,
|
||||
expected: expectedStatus || 200
|
||||
}
|
||||
)
|
||||
if (expectedPrice !== null) {
|
||||
expect(result.extra.price).toBeCloseTo(expectedPrice)
|
||||
}
|
||||
if (createInvoiceFlag && result.payment_request) {
|
||||
const decoded = await decodePayment(
|
||||
client.context,
|
||||
result.payment_request
|
||||
)
|
||||
expect(Number(result.extra.price_in_sats) * 1000).toBe(
|
||||
decoded.amount_msat
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const paidOne = await buyNip5Address(
|
||||
client.context,
|
||||
client,
|
||||
domainId,
|
||||
identifierOne,
|
||||
{
|
||||
pubkey:
|
||||
'04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfaaaab',
|
||||
years: 3,
|
||||
create_invoice: true
|
||||
}
|
||||
)
|
||||
await payInvoiceViaUi(admin, paidOne.payment_request)
|
||||
await getNostrJson(client.context, domainId, identifierOne)
|
||||
|
||||
const quoteThree = await buyNip5Address(
|
||||
client.context,
|
||||
client,
|
||||
domainId,
|
||||
identifierThree,
|
||||
{
|
||||
pubkey:
|
||||
'04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfaaaac',
|
||||
years: 5
|
||||
}
|
||||
)
|
||||
expect(quoteThree.extra.price).toBeCloseTo(500.0)
|
||||
const paidThree = await buyNip5Address(
|
||||
client.context,
|
||||
client,
|
||||
domainId,
|
||||
identifierThree,
|
||||
{
|
||||
pubkey:
|
||||
'04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfaaaac',
|
||||
years: 5,
|
||||
promo_code: 'code_alan',
|
||||
create_invoice: true
|
||||
}
|
||||
)
|
||||
await payInvoiceViaUi(admin, paidThree.payment_request)
|
||||
await getNostrJson(client.context, domainId, identifierThree)
|
||||
|
||||
const alanBalance = await getWallet(owner.context, alanWallet.inkey)
|
||||
const refererBonus = Math.floor(paidThree.extra.price_in_sats / 5)
|
||||
expect(
|
||||
Math.abs(Math.floor(alanBalance.balance / 1000) - refererBonus)
|
||||
).toBeLessThanOrEqual(100)
|
||||
})
|
||||
@@ -0,0 +1,252 @@
|
||||
const {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
state,
|
||||
apiKeyHeaders,
|
||||
getPayments,
|
||||
jsonRequest,
|
||||
newUser,
|
||||
newUserWithUi,
|
||||
getAdminSession,
|
||||
payInvoiceViaUi,
|
||||
ensureExtensionsInstalledViaUi,
|
||||
enableExtensionViaUi,
|
||||
createNip5Domain,
|
||||
createUserNip5Address,
|
||||
activateNip5Address,
|
||||
createAuctionRoom,
|
||||
updateAuctionRoom,
|
||||
createAuctionItem,
|
||||
bidOnItem,
|
||||
auctionNip5Webhooks
|
||||
} = require('./scenario-helpers')
|
||||
|
||||
setupIntegration()
|
||||
|
||||
test('011 auction house transfers NIP5 addresses through bids and fixed-price sale', async ({
|
||||
browser
|
||||
}) => {
|
||||
test.setTimeout(10 * 60 * 1000)
|
||||
|
||||
const admin = await getAdminSession(browser)
|
||||
await ensureExtensionsInstalledViaUi(admin, [
|
||||
'auction_house',
|
||||
'nostrnip5',
|
||||
'lnurlp'
|
||||
])
|
||||
await enableExtensionViaUi(admin, 'nostrnip5')
|
||||
await enableExtensionViaUi(admin, 'lnurlp')
|
||||
await jsonRequest(state.adminContext, 'put', '/nostrnip5/api/v1/settings', {
|
||||
headers: apiKeyHeaders(state.adminWallet.adminkey),
|
||||
data: {
|
||||
lnaddress_api_endpoint: state.baseURL,
|
||||
lnaddress_api_admin_key: state.adminWallet.adminkey
|
||||
}
|
||||
})
|
||||
|
||||
const owner = await newUserWithUi(browser, 'auction-owner')
|
||||
await enableExtensionViaUi(owner, 'auction_house')
|
||||
await enableExtensionViaUi(owner, 'nostrnip5')
|
||||
await enableExtensionViaUi(owner, 'lnurlp')
|
||||
const domain = await createNip5Domain(owner.context, owner, {
|
||||
cost_extra: {transfer_secret: '1234'}
|
||||
})
|
||||
const domainId = domain.id
|
||||
|
||||
await jsonRequest(owner.context, 'put', '/nostrnip5/api/v1/settings', {
|
||||
headers: apiKeyHeaders(owner.adminkey),
|
||||
data: {
|
||||
lnaddress_api_endpoint: state.baseURL,
|
||||
lnaddress_api_admin_key: state.adminWallet.adminkey
|
||||
}
|
||||
})
|
||||
|
||||
const seller = await newUserWithUi(browser, 'auction-seller')
|
||||
await enableExtensionViaUi(seller, 'auction_house')
|
||||
await enableExtensionViaUi(seller, 'nostrnip5')
|
||||
|
||||
const createdAddresses = []
|
||||
for (let index = 1; index <= 10; index++) {
|
||||
const address = await createUserNip5Address(
|
||||
seller.context,
|
||||
seller,
|
||||
domainId,
|
||||
`id_${index}__${domainId}`
|
||||
)
|
||||
await activateNip5Address(seller.context, seller, domainId, address.id)
|
||||
createdAddresses.push(address)
|
||||
}
|
||||
|
||||
const sellRoom = await createAuctionRoom(owner.context, owner, {
|
||||
name: 'nip5 sales',
|
||||
description: 'Auctions for Nip5',
|
||||
fee_wallet_id: owner.walletId,
|
||||
currency: 'USD',
|
||||
type: 'fixed_price'
|
||||
})
|
||||
await updateAuctionRoom(owner.context, owner, {
|
||||
id: sellRoom.id,
|
||||
name: 'nip5 sells',
|
||||
description: 'Sells for Nip5',
|
||||
currency: 'sat',
|
||||
type: 'fixed_price',
|
||||
room_percentage: 20,
|
||||
fee_wallet_id: owner.walletId,
|
||||
is_open_room: true,
|
||||
extra: auctionNip5Webhooks(domainId)
|
||||
})
|
||||
|
||||
const auctionRoom = await createAuctionRoom(owner.context, owner, {
|
||||
name: 'nip5 auctions',
|
||||
description: 'Auctions for Nip5',
|
||||
fee_wallet_id: owner.walletId,
|
||||
currency: 'USD',
|
||||
type: 'auction'
|
||||
})
|
||||
await updateAuctionRoom(owner.context, owner, {
|
||||
id: auctionRoom.id,
|
||||
name: 'nip5 auctions',
|
||||
description: 'Auctions for Nip5',
|
||||
currency: 'sat',
|
||||
type: 'auction',
|
||||
min_bid_up_percentage: 5,
|
||||
room_percentage: 10,
|
||||
fee_wallet_id: owner.walletId,
|
||||
is_open_room: true,
|
||||
extra: auctionNip5Webhooks(domainId)
|
||||
})
|
||||
|
||||
const sellerAddresses = await jsonRequest(
|
||||
seller.context,
|
||||
'get',
|
||||
'/nostrnip5/api/v1/user/addresses',
|
||||
{
|
||||
headers: apiKeyHeaders(seller.inkey),
|
||||
params: {active: true}
|
||||
}
|
||||
)
|
||||
expect(Array.isArray(sellerAddresses)).toBeTruthy()
|
||||
const addressForAuction = createdAddresses[0]
|
||||
const transfer = await jsonRequest(
|
||||
seller.context,
|
||||
'get',
|
||||
`/nostrnip5/api/v1/domain/${domainId}/address/${addressForAuction.id}/transfer`,
|
||||
{headers: apiKeyHeaders(seller.adminkey)}
|
||||
)
|
||||
expect(transfer.transfer_code).toBeTruthy()
|
||||
|
||||
const item = await createAuctionItem(seller.context, seller, auctionRoom.id, {
|
||||
name: addressForAuction.local_part,
|
||||
ask_price: 10.1,
|
||||
transfer_code: transfer.transfer_code
|
||||
})
|
||||
|
||||
let winningBidder
|
||||
for (let index = 1; index <= 15; index++) {
|
||||
const bidder = await newUser(`auction-bidder-${index}`)
|
||||
const bid = await bidOnItem(
|
||||
bidder.context,
|
||||
bidder,
|
||||
item.id,
|
||||
index * 100,
|
||||
`bid index: ${index}`
|
||||
)
|
||||
await payInvoiceViaUi(admin, bid.payment_request)
|
||||
winningBidder = bidder
|
||||
}
|
||||
|
||||
const itemAfterBids = await jsonRequest(
|
||||
owner.context,
|
||||
'get',
|
||||
`/auction_house/api/v1/items/${item.id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(owner.inkey)
|
||||
}
|
||||
)
|
||||
expect(itemAfterBids.bids.length).toBeGreaterThanOrEqual(15)
|
||||
await getPayments(owner.context, owner.inkey)
|
||||
await jsonRequest(
|
||||
owner.context,
|
||||
'delete',
|
||||
`/auction_house/api/v1/items/${item.id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(owner.adminkey),
|
||||
params: {force_close: true}
|
||||
}
|
||||
)
|
||||
const winnerAddresses = await jsonRequest(
|
||||
winningBidder.context,
|
||||
'get',
|
||||
'/nostrnip5/api/v1/user/addresses',
|
||||
{
|
||||
headers: apiKeyHeaders(winningBidder.inkey),
|
||||
params: {active: true}
|
||||
}
|
||||
)
|
||||
expect(JSON.stringify(winnerAddresses)).toContain(
|
||||
addressForAuction.local_part
|
||||
)
|
||||
|
||||
const fixedPriceAddress = createdAddresses[1]
|
||||
const fixedItems = await jsonRequest(
|
||||
seller.context,
|
||||
'get',
|
||||
`/auction_house/api/v1/items/${auctionRoom.id}/paginated`,
|
||||
{
|
||||
headers: apiKeyHeaders(seller.inkey),
|
||||
params: {
|
||||
sortby: 'ask_price',
|
||||
direction: 'asc',
|
||||
name: fixedPriceAddress.local_part
|
||||
}
|
||||
}
|
||||
)
|
||||
if (fixedItems.data?.[0]?.id) {
|
||||
await jsonRequest(
|
||||
seller.context,
|
||||
'delete',
|
||||
`/auction_house/api/v1/items/${fixedItems.data[0].id}`,
|
||||
{
|
||||
headers: apiKeyHeaders(seller.adminkey),
|
||||
params: {force_close: true}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const fixedTransfer = await jsonRequest(
|
||||
seller.context,
|
||||
'get',
|
||||
`/nostrnip5/api/v1/domain/${domainId}/address/${fixedPriceAddress.id}/transfer`,
|
||||
{headers: apiKeyHeaders(seller.adminkey)}
|
||||
)
|
||||
const sellItem = await createAuctionItem(
|
||||
seller.context,
|
||||
seller,
|
||||
sellRoom.id,
|
||||
{
|
||||
name: fixedPriceAddress.local_part,
|
||||
ask_price: 5000,
|
||||
transfer_code: fixedTransfer.transfer_code
|
||||
}
|
||||
)
|
||||
const buyer = await newUser('auction-buyer')
|
||||
const buy = await bidOnItem(
|
||||
buyer.context,
|
||||
buyer,
|
||||
sellItem.id,
|
||||
5000,
|
||||
'fixed price buy'
|
||||
)
|
||||
await payInvoiceViaUi(admin, buy.payment_request)
|
||||
const buyerAddresses = await jsonRequest(
|
||||
buyer.context,
|
||||
'get',
|
||||
'/nostrnip5/api/v1/user/addresses',
|
||||
{
|
||||
headers: apiKeyHeaders(buyer.inkey),
|
||||
params: {active: true}
|
||||
}
|
||||
)
|
||||
expect(JSON.stringify(buyerAddresses)).toContain(fixedPriceAddress.local_part)
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,866 @@
|
||||
const {test, expect} = require('@playwright/test')
|
||||
|
||||
const {
|
||||
apiKeyHeaders,
|
||||
clearMirror,
|
||||
createAccount,
|
||||
createWallet,
|
||||
decodePayment,
|
||||
expectStatus,
|
||||
fetchManifest,
|
||||
getPayment,
|
||||
getPayments,
|
||||
getWallet,
|
||||
initServer,
|
||||
jsonRequest,
|
||||
latestRelease,
|
||||
loginAdmin,
|
||||
lnurlScan,
|
||||
mirrorUrl,
|
||||
pageStatus,
|
||||
pollPayment,
|
||||
pollWalletBalance,
|
||||
responseJson,
|
||||
textRequest,
|
||||
topUpWallet,
|
||||
uninstallExtension
|
||||
} = require('./helpers')
|
||||
|
||||
const state = {
|
||||
baseURL: null,
|
||||
adminContext: null,
|
||||
adminWallet: null,
|
||||
adminSession: null,
|
||||
extensionById: null,
|
||||
contextFactory: null
|
||||
}
|
||||
|
||||
const contexts = []
|
||||
const browserContexts = []
|
||||
|
||||
async function newContext() {
|
||||
const context = await state.contextFactory.newContext({
|
||||
baseURL: state.baseURL,
|
||||
extraHTTPHeaders: {
|
||||
Accept: 'application/json, text/plain, */*'
|
||||
}
|
||||
})
|
||||
contexts.push(context)
|
||||
return context
|
||||
}
|
||||
|
||||
async function newUser(name) {
|
||||
const context = await newContext()
|
||||
const account = await createAccount(context, `${name}-${Date.now()}`)
|
||||
return {context, ...account}
|
||||
}
|
||||
|
||||
async function newBrowserSession(browser, account) {
|
||||
const uiContext = await browser.newContext({baseURL: state.baseURL})
|
||||
browserContexts.push(uiContext)
|
||||
if (account?.userId) {
|
||||
await textRequest(uiContext.request, 'get', '/')
|
||||
await jsonRequest(uiContext.request, 'post', '/api/v1/auth/usr', {
|
||||
data: {usr: account.userId}
|
||||
})
|
||||
} else {
|
||||
await loginAdmin(uiContext.request)
|
||||
}
|
||||
const page = await uiContext.newPage()
|
||||
page.setDefaultTimeout(45_000)
|
||||
return {uiContext, page}
|
||||
}
|
||||
|
||||
async function newUserWithUi(browser, name) {
|
||||
const user = await newUser(name)
|
||||
return {...user, ...(await newBrowserSession(browser, user))}
|
||||
}
|
||||
|
||||
async function getAdminSession(browser) {
|
||||
if (!state.adminSession) {
|
||||
state.adminSession = {
|
||||
...state.adminWallet,
|
||||
...(await newBrowserSession(browser))
|
||||
}
|
||||
}
|
||||
return state.adminSession
|
||||
}
|
||||
|
||||
function extensionName(extensionId) {
|
||||
return state.extensionById?.get(extensionId)?.name || extensionId
|
||||
}
|
||||
|
||||
async function refreshExtensionCatalog(context = state.adminContext) {
|
||||
const extensions = await jsonRequest(context, 'get', '/api/v1/extension')
|
||||
for (const extension of extensions) {
|
||||
const id = extension.id || extension.code
|
||||
if (id) {
|
||||
state.extensionById.set(id, {
|
||||
id,
|
||||
name: extension.name || extension.title || id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function responsePath(response) {
|
||||
return new URL(response.url()).pathname
|
||||
}
|
||||
|
||||
function responseMatches(response, method, path) {
|
||||
return (
|
||||
response.request().method() === method && responsePath(response) === path
|
||||
)
|
||||
}
|
||||
|
||||
async function clickAndCheckResponse(
|
||||
page,
|
||||
method,
|
||||
path,
|
||||
action,
|
||||
expected = 200,
|
||||
timeout = 45_000
|
||||
) {
|
||||
const responsePromise = page.waitForResponse(
|
||||
response => responseMatches(response, method, path),
|
||||
{timeout}
|
||||
)
|
||||
await action()
|
||||
const response = await responsePromise
|
||||
await expectStatus(response, expected, `${method} ${path}`)
|
||||
return response
|
||||
}
|
||||
|
||||
async function clickAndGetJsonResponse(
|
||||
page,
|
||||
method,
|
||||
path,
|
||||
action,
|
||||
expected = 200
|
||||
) {
|
||||
const response = await clickAndCheckResponse(
|
||||
page,
|
||||
method,
|
||||
path,
|
||||
action,
|
||||
expected
|
||||
)
|
||||
return responseJson(response)
|
||||
}
|
||||
|
||||
async function gotoPage(page, path, expected = 200) {
|
||||
const response = await page.goto(path)
|
||||
if (response) {
|
||||
await expectStatus(response, expected, `GET ${path}`)
|
||||
}
|
||||
await page.waitForFunction(() => window.LNbits && window.g)
|
||||
const credentialsButton = page.getByRole('button', {name: /i understand/i})
|
||||
if (await credentialsButton.isVisible({timeout: 1000}).catch(() => false)) {
|
||||
await credentialsButton.click()
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
async function gotoWallet(session, walletId = session.walletId) {
|
||||
await gotoPage(session.page, `/wallet/${walletId}`)
|
||||
await expect(
|
||||
session.page.getByRole('button', {name: /receive/i})
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
async function fillQuasarField(scope, label, value) {
|
||||
const field = scope
|
||||
.locator('.q-field')
|
||||
.filter({hasText: label})
|
||||
.locator('input, textarea')
|
||||
.first()
|
||||
await field.fill(String(value))
|
||||
}
|
||||
|
||||
async function createInvoiceViaUi(session, amount, memo) {
|
||||
await gotoWallet(session)
|
||||
await session.page.getByRole('button', {name: /receive/i}).click()
|
||||
const dialog = session.page
|
||||
.locator('.q-dialog')
|
||||
.filter({hasText: 'Create Invoice'})
|
||||
.last()
|
||||
await fillQuasarField(dialog, 'Amount (sat)', amount)
|
||||
await fillQuasarField(dialog, 'Memo', memo)
|
||||
const invoice = await clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'POST',
|
||||
'/api/v1/payments',
|
||||
() => dialog.getByRole('button', {name: /create invoice/i}).click(),
|
||||
[200, 201]
|
||||
)
|
||||
expect(invoice.bolt11).toBeTruthy()
|
||||
return invoice
|
||||
}
|
||||
|
||||
async function payInvoiceViaUi(session, bolt11, expected = 201) {
|
||||
await gotoWallet(session)
|
||||
await session.page.getByRole('button', {name: /send/i}).click()
|
||||
const dialog = session.page.locator('.q-dialog').last()
|
||||
await fillQuasarField(dialog, 'Paste an invoice', bolt11)
|
||||
await dialog.getByRole('button', {name: /read/i}).click()
|
||||
await expect(dialog.getByRole('button', {name: /^pay$/i})).toBeVisible()
|
||||
return clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'POST',
|
||||
'/api/v1/payments',
|
||||
() => dialog.getByRole('button', {name: /^pay$/i}).click(),
|
||||
expected
|
||||
)
|
||||
}
|
||||
|
||||
async function payLnurlViaUi(session, lnurl, amountMsat, comment) {
|
||||
await gotoWallet(session)
|
||||
await session.page.getByRole('button', {name: /send/i}).click()
|
||||
const dialog = session.page.locator('.q-dialog').last()
|
||||
await fillQuasarField(dialog, 'Paste an invoice', lnurl)
|
||||
await clickAndCheckResponse(session.page, 'POST', '/api/v1/lnurlscan', () =>
|
||||
dialog.getByRole('button', {name: /read/i}).click()
|
||||
)
|
||||
await fillQuasarField(dialog, 'Amount (sat)', amountMsat / 1000)
|
||||
if (comment) {
|
||||
await fillQuasarField(dialog, 'Comment (optional)', comment)
|
||||
}
|
||||
const payment = await clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'POST',
|
||||
'/api/v1/payments/lnurl',
|
||||
() => dialog.getByRole('button', {name: /^send$/i}).click()
|
||||
)
|
||||
expect(payment.payment_hash).toBeTruthy()
|
||||
return payment
|
||||
}
|
||||
|
||||
async function withdrawLnurlViaUi(
|
||||
session,
|
||||
lnurl,
|
||||
amount = 10,
|
||||
memo = 'withdraw 1',
|
||||
walletId = session.walletId
|
||||
) {
|
||||
await gotoWallet(session, walletId)
|
||||
await session.page.getByRole('button', {name: /send/i}).click()
|
||||
const scanDialog = session.page.locator('.q-dialog').last()
|
||||
await fillQuasarField(scanDialog, 'Paste an invoice', lnurl)
|
||||
await clickAndCheckResponse(session.page, 'POST', '/api/v1/lnurlscan', () =>
|
||||
scanDialog.getByRole('button', {name: /read/i}).click()
|
||||
)
|
||||
const withdrawDialog = session.page
|
||||
.locator('.q-dialog')
|
||||
.filter({hasText: 'Withdraw from'})
|
||||
.last()
|
||||
await fillQuasarField(withdrawDialog, 'Amount (sat)', amount)
|
||||
await fillQuasarField(withdrawDialog, 'Memo', memo)
|
||||
const payment = await clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'POST',
|
||||
'/api/v1/payments',
|
||||
() => withdrawDialog.getByRole('button', {name: /withdraw from/i}).click(),
|
||||
[200, 201]
|
||||
)
|
||||
expect(payment.payment_hash).toBeTruthy()
|
||||
return payment
|
||||
}
|
||||
|
||||
async function openExtensionsPage(session, tab = 'installed') {
|
||||
const extensionsLoaded = session.page
|
||||
.waitForResponse(response =>
|
||||
responseMatches(response, 'GET', '/api/v1/extension')
|
||||
)
|
||||
.catch(() => null)
|
||||
await gotoPage(session.page, '/extensions')
|
||||
await extensionsLoaded
|
||||
await session.page.waitForLoadState('networkidle').catch(() => null)
|
||||
await session.page.getByLabel(/search extensions/i).waitFor()
|
||||
const tabButton = session.page.getByRole('tab', {
|
||||
name: new RegExp(`^${tab}$`, 'i')
|
||||
})
|
||||
await tabButton.click()
|
||||
await expect(tabButton).toHaveAttribute('aria-selected', 'true')
|
||||
}
|
||||
|
||||
async function findExtensionCard(session, extensionId, tab = 'installed') {
|
||||
await openExtensionsPage(session, tab)
|
||||
const name = extensionName(extensionId)
|
||||
await session.page.getByLabel(/search extensions/i).fill(name)
|
||||
const card = session.page
|
||||
.locator('.q-card')
|
||||
.filter({
|
||||
has: session.page.locator('.text-h5').filter({
|
||||
hasText: new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`)
|
||||
})
|
||||
})
|
||||
.first()
|
||||
await expect(card).toBeVisible()
|
||||
return card
|
||||
}
|
||||
|
||||
async function openExtensionManager(session, extensionId, tab = 'installed') {
|
||||
const card = await findExtensionCard(session, extensionId, tab)
|
||||
await clickAndCheckResponse(
|
||||
session.page,
|
||||
'GET',
|
||||
`/api/v1/extension/${extensionId}/releases`,
|
||||
() => card.getByRole('button', {name: /manage/i}).click()
|
||||
)
|
||||
const dialog = session.page.locator('.q-dialog').filter({hasText: 'Releases'})
|
||||
await expect(dialog.last()).toBeVisible()
|
||||
return dialog.last()
|
||||
}
|
||||
|
||||
async function installExtensionVersionViaUi(session, extensionId, version) {
|
||||
let dialog
|
||||
try {
|
||||
dialog = await openExtensionManager(session, extensionId, 'installed')
|
||||
} catch {
|
||||
dialog = await openExtensionManager(session, extensionId, 'all')
|
||||
}
|
||||
const versionLabel = dialog.getByText(version, {exact: true}).first()
|
||||
if (!(await versionLabel.isVisible({timeout: 1000}).catch(() => false))) {
|
||||
await dialog
|
||||
.getByRole('button', {name: /expand/i})
|
||||
.first()
|
||||
.click()
|
||||
}
|
||||
await versionLabel.click()
|
||||
const installButton = dialog.getByRole('button', {name: /^install$/i}).first()
|
||||
if (!(await installButton.isVisible({timeout: 1000}).catch(() => false))) {
|
||||
await dialog.getByRole('button', {name: /close/i}).last().click()
|
||||
return null
|
||||
}
|
||||
const activatePromise = session.page
|
||||
.waitForResponse(response =>
|
||||
responseMatches(
|
||||
response,
|
||||
'PUT',
|
||||
`/api/v1/extension/${extensionId}/activate`
|
||||
)
|
||||
)
|
||||
.catch(() => null)
|
||||
let installResponsePromise = session.page.waitForResponse(response =>
|
||||
responseMatches(response, 'POST', '/api/v1/extension')
|
||||
)
|
||||
await installButton.click()
|
||||
const grantButton = session.page.getByRole('button', {
|
||||
name: /grant and install/i
|
||||
})
|
||||
if (await grantButton.isVisible({timeout: 1000}).catch(() => false)) {
|
||||
installResponsePromise.catch(() => null)
|
||||
installResponsePromise = session.page.waitForResponse(response =>
|
||||
responseMatches(response, 'POST', '/api/v1/extension')
|
||||
)
|
||||
await grantButton.click()
|
||||
}
|
||||
const installResponse = await installResponsePromise
|
||||
await expectStatus(installResponse, [200, 201], 'POST /api/v1/extension')
|
||||
const activateResponse = await activatePromise
|
||||
if (activateResponse) {
|
||||
await expectStatus(
|
||||
activateResponse,
|
||||
200,
|
||||
`PUT /api/v1/extension/${extensionId}/activate`
|
||||
)
|
||||
}
|
||||
return responseJson(installResponse)
|
||||
}
|
||||
|
||||
async function installLatestExtensionViaUi(session, extensionId) {
|
||||
const release = await latestRelease(session.uiContext.request, extensionId)
|
||||
return installExtensionVersionViaUi(session, extensionId, release.version)
|
||||
}
|
||||
|
||||
async function ensureExtensionsInstalledViaUi(session, extensionIds) {
|
||||
for (const extensionId of extensionIds) {
|
||||
const installed = await findExtensionCard(session, extensionId, 'installed')
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (!installed) {
|
||||
await installLatestExtensionViaUi(session, extensionId)
|
||||
}
|
||||
await setExtensionActiveViaUi(session, extensionId, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function enableExtensionViaUi(session, extensionId) {
|
||||
const card = await findExtensionCard(session, extensionId)
|
||||
const alreadyEnabled =
|
||||
(await card.getByRole('button', {name: /open/i}).isVisible()) ||
|
||||
(await card.getByRole('button', {name: /^disable$/i}).isVisible())
|
||||
if (alreadyEnabled) {
|
||||
return null
|
||||
}
|
||||
const payToEnableButton = card.getByRole('button', {name: /pay to enable/i})
|
||||
if (await payToEnableButton.isVisible()) {
|
||||
await payToEnableButton.click()
|
||||
const dialog = session.page
|
||||
.locator('.q-dialog')
|
||||
.filter({hasText: 'Recheck'})
|
||||
await expect(dialog.last()).toBeVisible()
|
||||
return clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'PUT',
|
||||
`/api/v1/extension/${extensionId}/enable`,
|
||||
() => session.page.getByText(/recheck/i).click()
|
||||
)
|
||||
}
|
||||
return clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'PUT',
|
||||
`/api/v1/extension/${extensionId}/enable`,
|
||||
() => card.getByRole('button', {name: /^enable$/i}).click()
|
||||
)
|
||||
}
|
||||
|
||||
async function disableExtensionViaUi(session, extensionId) {
|
||||
const card = await findExtensionCard(session, extensionId)
|
||||
if (!(await card.getByRole('button', {name: /^disable$/i}).isVisible())) {
|
||||
return null
|
||||
}
|
||||
return clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'PUT',
|
||||
`/api/v1/extension/${extensionId}/disable`,
|
||||
() => card.getByRole('button', {name: /^disable$/i}).click()
|
||||
)
|
||||
}
|
||||
|
||||
async function setExtensionActiveViaUi(
|
||||
session,
|
||||
extensionId,
|
||||
active,
|
||||
expected = 200
|
||||
) {
|
||||
const card = await findExtensionCard(session, extensionId)
|
||||
const action = active ? 'activate' : 'deactivate'
|
||||
const toggle = card.locator('.q-toggle').first()
|
||||
const alreadySet = await toggle
|
||||
.innerText()
|
||||
.then(text =>
|
||||
text.toLowerCase().includes(active ? 'activated' : 'deactivated')
|
||||
)
|
||||
if (alreadySet && expected === 200) return null
|
||||
return clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'PUT',
|
||||
`/api/v1/extension/${extensionId}/${action}`,
|
||||
() => toggle.click(),
|
||||
expected
|
||||
)
|
||||
}
|
||||
|
||||
async function payToEnableExtensionViaUi(session, extensionId) {
|
||||
const card = await findExtensionCard(session, extensionId)
|
||||
await card.getByRole('button', {name: /pay to enable/i}).click()
|
||||
const dialog = session.page.locator('.q-dialog').filter({hasText: 'Recheck'})
|
||||
await expect(dialog.last()).toBeVisible()
|
||||
const invoice = await clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'PUT',
|
||||
`/api/v1/extension/${extensionId}/invoice/enable`,
|
||||
() =>
|
||||
dialog
|
||||
.last()
|
||||
.getByRole('button', {name: /show qr/i})
|
||||
.click()
|
||||
)
|
||||
await dialog.last().getByRole('button', {name: /close/i}).click()
|
||||
await payInvoiceViaUi(session, invoice.payment_request || invoice.bolt11)
|
||||
const cardAfterPayment = await findExtensionCard(session, extensionId)
|
||||
if (
|
||||
await cardAfterPayment.getByRole('button', {name: /^enable$/i}).isVisible()
|
||||
) {
|
||||
return clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'PUT',
|
||||
`/api/v1/extension/${extensionId}/enable`,
|
||||
() => cardAfterPayment.getByRole('button', {name: /^enable$/i}).click()
|
||||
)
|
||||
}
|
||||
await cardAfterPayment.getByRole('button', {name: /pay to enable/i}).click()
|
||||
return clickAndGetJsonResponse(
|
||||
session.page,
|
||||
'PUT',
|
||||
`/api/v1/extension/${extensionId}/enable`,
|
||||
() => session.page.getByText(/recheck/i).click()
|
||||
)
|
||||
}
|
||||
|
||||
async function createPayLink(context, account, index, overrides = {}) {
|
||||
const data = compactObject({
|
||||
description: `receive payments ${index}`,
|
||||
min: 11,
|
||||
comment_chars: 128,
|
||||
webhook_url: mirrorUrl(),
|
||||
webhook_headers: '{"h1": "1"}',
|
||||
webhook_body: '{"b2": 2}',
|
||||
success_text: 'All goood!',
|
||||
success_url: 'https://lnbits.com',
|
||||
max: Number(`${index}1`),
|
||||
...overrides
|
||||
})
|
||||
const link = await jsonRequest(context, 'post', '/lnurlp/api/v1/links', {
|
||||
headers: apiKeyHeaders(account.adminkey),
|
||||
data,
|
||||
expected: 201
|
||||
})
|
||||
|
||||
expect(link.id).toBeTruthy()
|
||||
expect(link.wallet).toBe(account.walletId)
|
||||
return link
|
||||
}
|
||||
|
||||
async function createWithdrawLink(context, account, index, overrides = {}) {
|
||||
const link = await jsonRequest(context, 'post', '/withdraw/api/v1/links', {
|
||||
headers: apiKeyHeaders(account.adminkey),
|
||||
data: {
|
||||
is_unique: false,
|
||||
use_custom: false,
|
||||
title: `withdraw ${index}`,
|
||||
min_withdrawable: 10,
|
||||
max_withdrawable: index * 10,
|
||||
uses: 5,
|
||||
wait_time: 1,
|
||||
webhook_url: mirrorUrl(),
|
||||
webhook_headers: '{"h1": "1"}',
|
||||
webhook_body: '{"b2": 2}',
|
||||
custom_url: null,
|
||||
...overrides
|
||||
},
|
||||
expected: 201
|
||||
})
|
||||
|
||||
expect(link.id).toBeTruthy()
|
||||
expect(link.wallet).toBe(account.walletId)
|
||||
expect(link.title).toBe(`withdraw ${index}`)
|
||||
return link
|
||||
}
|
||||
|
||||
async function withdrawLnurl(
|
||||
context,
|
||||
receiveWallet,
|
||||
withdrawResponse,
|
||||
amount = 10,
|
||||
memo = 'withdraw 1'
|
||||
) {
|
||||
return jsonRequest(context, 'post', '/api/v1/payments', {
|
||||
headers: apiKeyHeaders(receiveWallet.inkey),
|
||||
data: {
|
||||
out: false,
|
||||
amount,
|
||||
memo,
|
||||
unit: 'sat',
|
||||
lnurl_withdraw: withdrawResponse
|
||||
},
|
||||
expected: 201
|
||||
})
|
||||
}
|
||||
|
||||
async function lndhubHeaders(accessToken) {
|
||||
return {Authorization: `Bearer ${accessToken}`}
|
||||
}
|
||||
|
||||
async function createNip5Domain(context, account, extra = {}) {
|
||||
const {cost_extra: extraCost = {}, ...extraFields} = extra
|
||||
return jsonRequest(context, 'post', '/nostrnip5/api/v1/domain', {
|
||||
headers: apiKeyHeaders(account.adminkey),
|
||||
data: {
|
||||
cost_extra: {
|
||||
max_years: '3',
|
||||
char_count_cost: [],
|
||||
rank_cost: [],
|
||||
...extraCost
|
||||
},
|
||||
wallet: account.walletId,
|
||||
currency: 'USD',
|
||||
domain: `nostr-${Date.now()}.com`,
|
||||
cost: '100',
|
||||
...extraFields
|
||||
},
|
||||
expected: 201
|
||||
})
|
||||
}
|
||||
|
||||
async function createUserNip5Address(context, account, domainId, localPart) {
|
||||
const address = await jsonRequest(
|
||||
context,
|
||||
'post',
|
||||
`/nostrnip5/api/v1/user/domain/${domainId}/address`,
|
||||
{
|
||||
headers: apiKeyHeaders(account.adminkey),
|
||||
data: {
|
||||
config: {relays: ['relay.nostr.com']},
|
||||
pubkey:
|
||||
'04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
|
||||
local_part: localPart,
|
||||
domain_id: domainId
|
||||
},
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(address.id).toBeTruthy()
|
||||
return address
|
||||
}
|
||||
|
||||
async function createNip5Address(
|
||||
context,
|
||||
account,
|
||||
domainId,
|
||||
localPart,
|
||||
pubkey
|
||||
) {
|
||||
const address = await jsonRequest(
|
||||
context,
|
||||
'post',
|
||||
`/nostrnip5/api/v1/domain/${domainId}/address`,
|
||||
{
|
||||
headers: apiKeyHeaders(account.adminkey),
|
||||
data: {
|
||||
config: {relays: ['relay.nostr.com']},
|
||||
pubkey,
|
||||
local_part: localPart,
|
||||
domain_id: domainId
|
||||
},
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(address.id).toBeTruthy()
|
||||
return address
|
||||
}
|
||||
|
||||
async function activateNip5Address(context, account, domainId, addressId) {
|
||||
return jsonRequest(
|
||||
context,
|
||||
'put',
|
||||
`/nostrnip5/api/v1/domain/${domainId}/address/${addressId}/activate`,
|
||||
{
|
||||
headers: apiKeyHeaders(account.adminkey)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function getNostrJson(context, domainId, name, expected = 200) {
|
||||
return jsonRequest(
|
||||
context,
|
||||
'get',
|
||||
`/nostrnip5/api/v1/domain/${domainId}/nostr.json`,
|
||||
{
|
||||
params: {name},
|
||||
expected
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function buyNip5Address(context, account, domainId, localPart, options) {
|
||||
return jsonRequest(
|
||||
context,
|
||||
'post',
|
||||
`/nostrnip5/api/v1/user/domain/${domainId}/address`,
|
||||
{
|
||||
headers: apiKeyHeaders(account.adminkey),
|
||||
data: {
|
||||
domain_id: domainId,
|
||||
local_part: localPart,
|
||||
pubkey: options.pubkey,
|
||||
years: options.years,
|
||||
promo_code: options.promo_code ?? null,
|
||||
referer: options.referer ?? null,
|
||||
create_invoice: options.create_invoice ?? false
|
||||
},
|
||||
expected: options.expected ?? 200
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function createAuctionRoom(context, owner, data) {
|
||||
const room = await jsonRequest(
|
||||
context,
|
||||
'post',
|
||||
'/auction_house/api/v1/auction_room',
|
||||
{
|
||||
headers: apiKeyHeaders(owner.adminkey),
|
||||
data,
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(room.id).toBeTruthy()
|
||||
return room
|
||||
}
|
||||
|
||||
async function updateAuctionRoom(context, owner, data) {
|
||||
const room = await jsonRequest(
|
||||
context,
|
||||
'put',
|
||||
'/auction_house/api/v1/auction_room',
|
||||
{
|
||||
headers: apiKeyHeaders(owner.adminkey),
|
||||
data
|
||||
}
|
||||
)
|
||||
expect(room.id).toBe(data.id)
|
||||
return room
|
||||
}
|
||||
|
||||
async function createAuctionItem(context, seller, roomId, data) {
|
||||
const item = await jsonRequest(
|
||||
context,
|
||||
'post',
|
||||
`/auction_house/api/v1/items/${roomId}`,
|
||||
{
|
||||
headers: apiKeyHeaders(seller.adminkey),
|
||||
data,
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(item.id).toBeTruthy()
|
||||
return item
|
||||
}
|
||||
|
||||
async function bidOnItem(context, bidder, itemId, amount, memo) {
|
||||
const bid = await jsonRequest(
|
||||
context,
|
||||
'put',
|
||||
`/auction_house/api/v1/bids/${itemId}`,
|
||||
{
|
||||
headers: apiKeyHeaders(bidder.adminkey),
|
||||
data: {amount, ln_address: '', memo},
|
||||
expected: 201
|
||||
}
|
||||
)
|
||||
expect(bid.payment_request).toBeTruthy()
|
||||
return bid
|
||||
}
|
||||
|
||||
function setupIntegration() {
|
||||
test.beforeAll(async ({playwright}, testInfo) => {
|
||||
state.baseURL = testInfo.project.use.baseURL
|
||||
state.contextFactory = playwright.request
|
||||
state.adminSession = null
|
||||
state.adminContext = await newContext()
|
||||
state.adminWallet = await initServer(state.adminContext)
|
||||
const manifest = await fetchManifest()
|
||||
state.extensionById = new Map(
|
||||
manifest.extensions.map(extension => [extension.id, extension])
|
||||
)
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await Promise.all(contexts.map(context => context.dispose()))
|
||||
await Promise.all(browserContexts.map(context => context.close()))
|
||||
contexts.splice(0, contexts.length)
|
||||
browserContexts.splice(0, browserContexts.length)
|
||||
state.baseURL = null
|
||||
state.adminContext = null
|
||||
state.adminWallet = null
|
||||
state.adminSession = null
|
||||
state.extensionById = null
|
||||
state.contextFactory = null
|
||||
})
|
||||
}
|
||||
|
||||
function compactObject(value) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).filter(([, entry]) => entry !== undefined)
|
||||
)
|
||||
}
|
||||
|
||||
function auctionNip5Webhooks(domainId) {
|
||||
return {
|
||||
duration: {days: 7, hours: 0, minutes: 0},
|
||||
lock_webhook: {
|
||||
method: 'PUT',
|
||||
url: `${state.baseURL}/nostrnip5/api/v1/domain/${domainId}/address/lock`,
|
||||
headers: '',
|
||||
data: '{\n "transfer_code": "${transfer_code}"\n}'
|
||||
},
|
||||
unlock_webhook: {
|
||||
method: 'PUT',
|
||||
url: `${state.baseURL}/nostrnip5/api/v1/domain/${domainId}/address/unlock`,
|
||||
headers: '',
|
||||
data: '{\n "lock_code": "${lock_code}"\n}'
|
||||
},
|
||||
transfer_webhook: {
|
||||
method: 'PUT',
|
||||
url: `${state.baseURL}/nostrnip5/api/v1/domain/${domainId}/address/transfer`,
|
||||
headers: '',
|
||||
data: '{\n "lock_code": "${lock_code}",\n "new_owner_id": "${new_owner_id}"\n}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
test,
|
||||
expect,
|
||||
setupIntegration,
|
||||
state,
|
||||
apiKeyHeaders,
|
||||
clearMirror,
|
||||
createAccount,
|
||||
createWallet,
|
||||
decodePayment,
|
||||
expectStatus,
|
||||
fetchManifest,
|
||||
getPayment,
|
||||
getPayments,
|
||||
getWallet,
|
||||
initServer,
|
||||
jsonRequest,
|
||||
latestRelease,
|
||||
loginAdmin,
|
||||
lnurlScan,
|
||||
mirrorUrl,
|
||||
pageStatus,
|
||||
pollPayment,
|
||||
pollWalletBalance,
|
||||
responseJson,
|
||||
textRequest,
|
||||
topUpWallet,
|
||||
uninstallExtension,
|
||||
newContext,
|
||||
newUser,
|
||||
newBrowserSession,
|
||||
newUserWithUi,
|
||||
getAdminSession,
|
||||
extensionName,
|
||||
refreshExtensionCatalog,
|
||||
responsePath,
|
||||
responseMatches,
|
||||
clickAndCheckResponse,
|
||||
clickAndGetJsonResponse,
|
||||
gotoPage,
|
||||
gotoWallet,
|
||||
fillQuasarField,
|
||||
createInvoiceViaUi,
|
||||
payInvoiceViaUi,
|
||||
payLnurlViaUi,
|
||||
withdrawLnurlViaUi,
|
||||
openExtensionsPage,
|
||||
findExtensionCard,
|
||||
openExtensionManager,
|
||||
installExtensionVersionViaUi,
|
||||
installLatestExtensionViaUi,
|
||||
ensureExtensionsInstalledViaUi,
|
||||
enableExtensionViaUi,
|
||||
disableExtensionViaUi,
|
||||
setExtensionActiveViaUi,
|
||||
payToEnableExtensionViaUi,
|
||||
createPayLink,
|
||||
createWithdrawLink,
|
||||
withdrawLnurl,
|
||||
lndhubHeaders,
|
||||
createNip5Domain,
|
||||
createUserNip5Address,
|
||||
createNip5Address,
|
||||
activateNip5Address,
|
||||
getNostrJson,
|
||||
buyNip5Address,
|
||||
createAuctionRoom,
|
||||
updateAuctionRoom,
|
||||
createAuctionItem,
|
||||
bidOnItem,
|
||||
compactObject,
|
||||
auctionNip5Webhooks
|
||||
}
|
||||
Reference in New Issue
Block a user