feat: first migration
This commit is contained in:
@@ -85,15 +85,15 @@ jobs:
|
|||||||
secrets:
|
secrets:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
jmeter:
|
integration:
|
||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.12"]
|
python-version: ["3.12"]
|
||||||
uses: ./.github/workflows/jmeter.yml
|
uses: ./.github/workflows/integration.yml
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
bundle:
|
bundle:
|
||||||
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi, regtest, jmeter ]
|
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi, regtest, integration ]
|
||||||
uses: ./.github/workflows/bundle.yml
|
uses: ./.github/workflows/bundle.yml
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Extension Integration Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
python-version:
|
||||||
|
description: "Python Version"
|
||||||
|
required: true
|
||||||
|
default: "3.10"
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
integration:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: ./.github/actions/prepare
|
||||||
|
with:
|
||||||
|
python-version: ${{ inputs.python-version }}
|
||||||
|
npm: "true"
|
||||||
|
|
||||||
|
- name: run Playwright integration tests
|
||||||
|
env:
|
||||||
|
EXTENSIONS_MANIFEST_URL: "https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json"
|
||||||
|
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly,satspay,tipjar,tpos,lnurlp,withdraw"
|
||||||
|
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||||
|
AUTH_HTTPS_ONLY: false
|
||||||
|
run: npm run test:integration
|
||||||
|
|
||||||
|
- name: print lnbits log
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
sleep 1
|
||||||
|
cat tests/data/integration/logs/debug.log || true
|
||||||
|
|
||||||
|
- name: upload integration test results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: extension-integration-test-results
|
||||||
|
path: |
|
||||||
|
playwright-report/integration/
|
||||||
|
test-results/
|
||||||
|
tests/data/integration/logs/
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
name: JMeter Extension Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
python-version:
|
|
||||||
description: "Python Version"
|
|
||||||
required: true
|
|
||||||
default: "3.10"
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
jmeter:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: ./.github/actions/prepare
|
|
||||||
with:
|
|
||||||
python-version: ${{ inputs.python-version }}
|
|
||||||
|
|
||||||
- name: run LNbits
|
|
||||||
env:
|
|
||||||
LNBITS_ADMIN_UI: true
|
|
||||||
AUTH_HTTPS_ONLY: false
|
|
||||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
|
||||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
|
||||||
run: |
|
|
||||||
uv run lnbits &
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
- name: setup java version
|
|
||||||
run: |
|
|
||||||
update-java-alternatives --list
|
|
||||||
sudo update-java-alternatives --set /usr/lib/jvm/temurin-8-jdk-amd64
|
|
||||||
java -version
|
|
||||||
|
|
||||||
- name: clone lnbits-extensions, install jmeter and run tests
|
|
||||||
env:
|
|
||||||
JAVA_HOME: /usr/lib/jvm/temurin-8-jdk-amd64
|
|
||||||
EXTENSIONS_MANIFEST_PATH: "/lnbits/lnbits-extensions/refs/heads/main/extensions.json"
|
|
||||||
run: |
|
|
||||||
git clone https://github.com/lnbits/lnbits-extensions
|
|
||||||
cd lnbits-extensions
|
|
||||||
mkdir logs
|
|
||||||
mkdir reports
|
|
||||||
make install-jmeter
|
|
||||||
make start-mirror-server
|
|
||||||
make test
|
|
||||||
|
|
||||||
- name: print lnbits log
|
|
||||||
if: ${{ always() }}
|
|
||||||
run: |
|
|
||||||
# catch up time for lnbits
|
|
||||||
sleep 1
|
|
||||||
cat data/logs/debug.log
|
|
||||||
|
|
||||||
- name: upload jmeter test results
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: jmeter-extension-test-results
|
|
||||||
path: |
|
|
||||||
lnbits-extensions/reports/
|
|
||||||
lnbits-extensions/logs/
|
|
||||||
@@ -18,6 +18,8 @@ __pycache__
|
|||||||
.webassets-cache
|
.webassets-cache
|
||||||
htmlcov
|
htmlcov
|
||||||
test-reports
|
test-reports
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
tests/data/*.sqlite3
|
tests/data/*.sqlite3
|
||||||
|
|
||||||
*.swo
|
*.swo
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: test
|
.PHONY: test test-integration
|
||||||
|
|
||||||
all: format check
|
all: format check
|
||||||
|
|
||||||
@@ -69,6 +69,9 @@ test-regtest:
|
|||||||
rm -rf ./tests/data \
|
rm -rf ./tests/data \
|
||||||
uv run pytest tests/regtest
|
uv run pytest tests/regtest
|
||||||
|
|
||||||
|
test-integration:
|
||||||
|
npm run test:integration
|
||||||
|
|
||||||
test-migration:
|
test-migration:
|
||||||
LNBITS_ADMIN_UI=True \
|
LNBITS_ADMIN_UI=True \
|
||||||
make test-api
|
make test-api
|
||||||
|
|||||||
Generated
+64
@@ -21,6 +21,7 @@
|
|||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"clean-css-cli": "^5.6.3",
|
"clean-css-cli": "^5.6.3",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
@@ -576,6 +577,22 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@scure/base": {
|
"node_modules/@scure/base": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
|
||||||
@@ -1754,6 +1771,53 @@
|
|||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.14",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lnbits",
|
"name": "lnbits",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test:integration": "playwright test --config=playwright.integration.config.js",
|
||||||
"sass": "./node_modules/.bin/sass ./lnbits/static/scss/base.scss > ./lnbits/static/css/base.css",
|
"sass": "./node_modules/.bin/sass ./lnbits/static/scss/base.scss > ./lnbits/static/css/base.css",
|
||||||
"vendor_copy": "node -e \"require('./package.json').vendor.forEach((file) => require('fs').copyFileSync(file, './lnbits/static/vendor/'+file.split('/').pop()))\"",
|
"vendor_copy": "node -e \"require('./package.json').vendor.forEach((file) => require('fs').copyFileSync(file, './lnbits/static/vendor/'+file.split('/').pop()))\"",
|
||||||
"vendor_json": "node -e \"require('fs').writeFileSync('./lnbits/static/vendor.json', JSON.stringify(require('./package.json').bundle))\"",
|
"vendor_json": "node -e \"require('fs').writeFileSync('./lnbits/static/vendor.json', JSON.stringify(require('./package.json').bundle))\"",
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
"bundle": "npm run sass && npm run vendor_copy && npm run vendor_json && npm run vendor_bundle_css && npm run vendor_bundle_js && npm run vendor_bundle_components && npm run vendor_minify_css && npm run vendor_minify_js && npm run vendor_minify_components"
|
"bundle": "npm run sass && npm run vendor_copy && npm run vendor_json && npm run vendor_bundle_css && npm run vendor_bundle_js && npm run vendor_bundle_components && npm run vendor_minify_css && npm run vendor_minify_js && npm run vendor_minify_components"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"clean-css-cli": "^5.6.3",
|
"clean-css-cli": "^5.6.3",
|
||||||
"concat": "^1.0.3",
|
"concat": "^1.0.3",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const {defineConfig} = require('@playwright/test')
|
||||||
|
|
||||||
|
const port = process.env.PORT || '5000'
|
||||||
|
const baseURL = process.env.LNBITS_BASE_URL || `http://127.0.0.1:${port}`
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
testDir: './tests/integration',
|
||||||
|
testMatch: '**/*.spec.js',
|
||||||
|
timeout: 120_000,
|
||||||
|
globalTimeout: 60 * 60 * 1000,
|
||||||
|
fullyParallel: false,
|
||||||
|
workers: 1,
|
||||||
|
reporter: [
|
||||||
|
['list'],
|
||||||
|
['html', {outputFolder: 'playwright-report/integration', open: 'never'}]
|
||||||
|
],
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
extraHTTPHeaders: {
|
||||||
|
Accept: 'application/json, text/plain, */*'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
webServer: process.env.LNBITS_SKIP_WEB_SERVER
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
command: 'node tests/integration/server.cjs',
|
||||||
|
url: baseURL,
|
||||||
|
timeout: 180_000,
|
||||||
|
reuseExistingServer: !process.env.CI
|
||||||
|
}
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,384 @@
|
|||||||
|
const {expect} = require('@playwright/test')
|
||||||
|
|
||||||
|
const DEFAULT_MANIFEST_URL =
|
||||||
|
'https://raw.githubusercontent.com/lnbits/lnbits-extensions/main/extensions.json'
|
||||||
|
|
||||||
|
function apiKeyHeaders(key) {
|
||||||
|
return key ? {'X-Api-Key': key} : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectStatus(response, expected, label) {
|
||||||
|
const statuses = Array.isArray(expected) ? expected : [expected]
|
||||||
|
if (!statuses.includes(response.status())) {
|
||||||
|
const body = await response.text()
|
||||||
|
throw new Error(
|
||||||
|
`${label || response.url()} expected status ${statuses.join(
|
||||||
|
'/'
|
||||||
|
)}, got ${response.status()}: ${body.slice(0, 800)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function responseJson(response) {
|
||||||
|
const text = await response.text()
|
||||||
|
if (!text) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected JSON from ${response.url()}: ${text.slice(0, 800)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function jsonRequest(context, method, url, options = {}) {
|
||||||
|
const response = await context[method](url, {
|
||||||
|
data: options.data,
|
||||||
|
form: options.form,
|
||||||
|
headers: options.headers,
|
||||||
|
params: options.params,
|
||||||
|
failOnStatusCode: false,
|
||||||
|
maxRedirects: options.maxRedirects
|
||||||
|
})
|
||||||
|
await expectStatus(
|
||||||
|
response,
|
||||||
|
options.expected ?? 200,
|
||||||
|
`${method.toUpperCase()} ${url}`
|
||||||
|
)
|
||||||
|
return responseJson(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function textRequest(context, method, url, options = {}) {
|
||||||
|
const response = await context[method](url, {
|
||||||
|
data: options.data,
|
||||||
|
headers: options.headers,
|
||||||
|
params: options.params,
|
||||||
|
failOnStatusCode: false,
|
||||||
|
maxRedirects: options.maxRedirects
|
||||||
|
})
|
||||||
|
await expectStatus(
|
||||||
|
response,
|
||||||
|
options.expected ?? 200,
|
||||||
|
`${method.toUpperCase()} ${url}`
|
||||||
|
)
|
||||||
|
return response.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginAdmin(context) {
|
||||||
|
await jsonRequest(context, 'post', '/api/v1/auth', {
|
||||||
|
data: {username: 'admin', password: 'secret1234'}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initServer(context) {
|
||||||
|
const home = await context.get('/', {
|
||||||
|
failOnStatusCode: false,
|
||||||
|
maxRedirects: 0
|
||||||
|
})
|
||||||
|
const location = home.headers().location || ''
|
||||||
|
if (
|
||||||
|
[301, 302, 303, 307, 308].includes(home.status()) &&
|
||||||
|
location.includes('first_install')
|
||||||
|
) {
|
||||||
|
await jsonRequest(context, 'put', '/api/v1/auth/first_install', {
|
||||||
|
data: {
|
||||||
|
username: 'admin',
|
||||||
|
password: 'secret1234',
|
||||||
|
password_repeat: 'secret1234'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await loginAdmin(context)
|
||||||
|
|
||||||
|
const wallets = await jsonRequest(context, 'get', '/api/v1/wallets')
|
||||||
|
expect(wallets.length).toBeGreaterThan(0)
|
||||||
|
const adminWallet = wallets[0]
|
||||||
|
|
||||||
|
await jsonRequest(context, 'put', '/admin/api/v1/settings', {
|
||||||
|
data: {lnbits_callback_url_rules: []}
|
||||||
|
})
|
||||||
|
await topUpWallet(context, adminWallet.id, 1_000_000)
|
||||||
|
|
||||||
|
return {
|
||||||
|
walletId: adminWallet.id,
|
||||||
|
inkey: adminWallet.inkey,
|
||||||
|
adminkey: adminWallet.adminkey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout(context) {
|
||||||
|
await jsonRequest(context, 'post', '/api/v1/auth/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAccount(context, name) {
|
||||||
|
await textRequest(context, 'get', '/')
|
||||||
|
const wallet = await jsonRequest(context, 'post', '/api/v1/account', {
|
||||||
|
data: {name}
|
||||||
|
})
|
||||||
|
await jsonRequest(context, 'post', '/api/v1/auth/usr', {
|
||||||
|
data: {usr: wallet.user}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
userId: wallet.user,
|
||||||
|
walletId: wallet.id,
|
||||||
|
inkey: wallet.inkey,
|
||||||
|
adminkey: wallet.adminkey,
|
||||||
|
raw: wallet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWallet(context, name) {
|
||||||
|
const wallet = await jsonRequest(context, 'post', '/api/v1/wallet', {
|
||||||
|
data: {name}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
userId: wallet.user,
|
||||||
|
walletId: wallet.id,
|
||||||
|
inkey: wallet.inkey,
|
||||||
|
adminkey: wallet.adminkey,
|
||||||
|
raw: wallet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function topUpWallet(adminContext, walletId, amount) {
|
||||||
|
return jsonRequest(adminContext, 'put', '/users/api/v1/balance', {
|
||||||
|
data: {amount: String(amount), id: walletId}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enableExtension(context, extension) {
|
||||||
|
const enabled = await jsonRequest(
|
||||||
|
context,
|
||||||
|
'put',
|
||||||
|
`/api/v1/extension/${extension}/enable`,
|
||||||
|
{
|
||||||
|
form: {enable: extension}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (enabled && Object.prototype.hasOwnProperty.call(enabled, 'is_enabled')) {
|
||||||
|
expect(enabled.is_enabled).toBeTruthy()
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disableExtension(context, extension) {
|
||||||
|
return jsonRequest(context, 'put', `/api/v1/extension/${extension}/disable`, {
|
||||||
|
form: {enable: extension}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deactivateExtension(context, extension) {
|
||||||
|
return jsonRequest(
|
||||||
|
context,
|
||||||
|
'put',
|
||||||
|
`/api/v1/extension/${extension}/deactivate`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activateExtension(context, extension, expected = 200) {
|
||||||
|
return jsonRequest(
|
||||||
|
context,
|
||||||
|
'put',
|
||||||
|
`/api/v1/extension/${extension}/activate`,
|
||||||
|
{
|
||||||
|
expected
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uninstallExtension(context, extension) {
|
||||||
|
const response = await context.delete(`/api/v1/extension/${extension}`, {
|
||||||
|
failOnStatusCode: false
|
||||||
|
})
|
||||||
|
await expectStatus(
|
||||||
|
response,
|
||||||
|
[200, 204, 404],
|
||||||
|
`DELETE /api/v1/extension/${extension}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pageStatus(context, path, expected = 200) {
|
||||||
|
const response = await context.get(path, {failOnStatusCode: false})
|
||||||
|
await expectStatus(response, expected, `GET ${path}`)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWallet(context, inkey) {
|
||||||
|
return jsonRequest(context, 'get', '/api/v1/wallet', {
|
||||||
|
headers: apiKeyHeaders(inkey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPayments(context, inkey, params) {
|
||||||
|
return jsonRequest(context, 'get', '/api/v1/payments', {
|
||||||
|
headers: apiKeyHeaders(inkey),
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createInvoice(context, adminkey, amount, memo) {
|
||||||
|
return jsonRequest(context, 'post', '/api/v1/payments', {
|
||||||
|
headers: apiKeyHeaders(adminkey),
|
||||||
|
data: {out: false, amount, memo}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function payInvoice(context, adminkey, bolt11, expected = 201) {
|
||||||
|
return jsonRequest(context, 'post', '/api/v1/payments', {
|
||||||
|
headers: apiKeyHeaders(adminkey),
|
||||||
|
data: {out: true, bolt11},
|
||||||
|
expected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPayment(context, inkey, paymentHash) {
|
||||||
|
return jsonRequest(context, 'get', `/api/v1/payments/${paymentHash}`, {
|
||||||
|
headers: apiKeyHeaders(inkey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decodePayment(context, data) {
|
||||||
|
return jsonRequest(context, 'post', '/api/v1/payments/decode', {
|
||||||
|
data: {data}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lnurlScan(context, lnurl) {
|
||||||
|
return jsonRequest(
|
||||||
|
context,
|
||||||
|
'get',
|
||||||
|
`/api/v1/lnurlscan/${encodeURIComponent(lnurl)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearMirror() {
|
||||||
|
const response = await fetch(mirrorUrl('/__mirror__/clear'))
|
||||||
|
if (response.status !== 204) {
|
||||||
|
throw new Error(`Expected mirror clear status 204, got ${response.status}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMirrorRequests() {
|
||||||
|
const response = await fetch(mirrorUrl('/__mirror__/requests'))
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Cannot fetch mirror requests: ${response.status}`)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollPayment(
|
||||||
|
context,
|
||||||
|
inkey,
|
||||||
|
paymentHash,
|
||||||
|
predicate,
|
||||||
|
timeout = 10_000
|
||||||
|
) {
|
||||||
|
let last
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
last = await getPayment(context, inkey, paymentHash)
|
||||||
|
return predicate(last)
|
||||||
|
},
|
||||||
|
{timeout}
|
||||||
|
)
|
||||||
|
.toBeTruthy()
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollWalletBalance(
|
||||||
|
context,
|
||||||
|
inkey,
|
||||||
|
expectedBalance,
|
||||||
|
timeout = 10_000
|
||||||
|
) {
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const wallet = await getWallet(context, inkey)
|
||||||
|
return wallet.balance
|
||||||
|
},
|
||||||
|
{timeout}
|
||||||
|
)
|
||||||
|
.toBe(expectedBalance)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchManifest() {
|
||||||
|
const manifestUrl =
|
||||||
|
process.env.EXTENSIONS_MANIFEST_URL || DEFAULT_MANIFEST_URL
|
||||||
|
const response = await fetch(manifestUrl)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot fetch extension manifest ${manifestUrl}: ${response.status}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function latestRelease(context, extension) {
|
||||||
|
const releases = await jsonRequest(
|
||||||
|
context,
|
||||||
|
'get',
|
||||||
|
`/api/v1/extension/${extension}/releases`
|
||||||
|
)
|
||||||
|
expect(releases.length).toBeGreaterThan(0)
|
||||||
|
return releases
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.version.localeCompare(b.version, undefined, {numeric: true})
|
||||||
|
)
|
||||||
|
.at(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installLatestExtension(context, extension) {
|
||||||
|
const release = await latestRelease(context, extension)
|
||||||
|
return jsonRequest(context, 'post', '/api/v1/extension', {
|
||||||
|
data: {
|
||||||
|
ext_id: extension,
|
||||||
|
archive: release.archive,
|
||||||
|
source_repo: release.source_repo,
|
||||||
|
version: release.version
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function mirrorUrl(path = '') {
|
||||||
|
const port = process.env.MIRROR_PORT || '8500'
|
||||||
|
return `http://localhost:${port}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apiKeyHeaders,
|
||||||
|
activateExtension,
|
||||||
|
clearMirror,
|
||||||
|
createAccount,
|
||||||
|
createInvoice,
|
||||||
|
createWallet,
|
||||||
|
deactivateExtension,
|
||||||
|
decodePayment,
|
||||||
|
disableExtension,
|
||||||
|
enableExtension,
|
||||||
|
expectStatus,
|
||||||
|
fetchManifest,
|
||||||
|
getMirrorRequests,
|
||||||
|
getPayment,
|
||||||
|
getPayments,
|
||||||
|
getWallet,
|
||||||
|
initServer,
|
||||||
|
installLatestExtension,
|
||||||
|
jsonRequest,
|
||||||
|
latestRelease,
|
||||||
|
lnurlScan,
|
||||||
|
loginAdmin,
|
||||||
|
logout,
|
||||||
|
mirrorUrl,
|
||||||
|
pageStatus,
|
||||||
|
payInvoice,
|
||||||
|
pollPayment,
|
||||||
|
pollWalletBalance,
|
||||||
|
responseJson,
|
||||||
|
textRequest,
|
||||||
|
topUpWallet,
|
||||||
|
uninstallExtension
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const http = require('http')
|
||||||
|
const path = require('path')
|
||||||
|
const {spawn} = require('child_process')
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..', '..')
|
||||||
|
const host = process.env.HOST || '127.0.0.1'
|
||||||
|
const port = process.env.PORT || '5000'
|
||||||
|
const mirrorPort = process.env.MIRROR_PORT || '8500'
|
||||||
|
const dataFolder =
|
||||||
|
process.env.LNBITS_DATA_FOLDER ||
|
||||||
|
path.join(root, 'tests', 'data', 'integration')
|
||||||
|
|
||||||
|
if (process.env.LNBITS_INTEGRATION_RESET_DATA !== 'false') {
|
||||||
|
fs.rmSync(dataFolder, {recursive: true, force: true})
|
||||||
|
}
|
||||||
|
fs.mkdirSync(dataFolder, {recursive: true})
|
||||||
|
|
||||||
|
const mirrorRequests = []
|
||||||
|
|
||||||
|
function readBody(req) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = []
|
||||||
|
req.on('data', chunk => chunks.push(chunk))
|
||||||
|
req.on('error', reject)
|
||||||
|
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBody(body) {
|
||||||
|
if (!body) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(body)
|
||||||
|
} catch {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mirror = http.createServer(async (req, res) => {
|
||||||
|
const bodyText = await readBody(req)
|
||||||
|
const body = parseBody(bodyText)
|
||||||
|
const record = {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
headers: req.headers,
|
||||||
|
body,
|
||||||
|
bodyText,
|
||||||
|
receivedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === '/__mirror__/clear') {
|
||||||
|
mirrorRequests.splice(0, mirrorRequests.length)
|
||||||
|
res.writeHead(204)
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === '/__mirror__/requests') {
|
||||||
|
res.writeHead(200, {'content-type': 'application/json'})
|
||||||
|
res.end(JSON.stringify(mirrorRequests))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mirrorRequests.push(record)
|
||||||
|
res.writeHead(200, {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
h1: '1'
|
||||||
|
})
|
||||||
|
res.end(JSON.stringify(body ?? record))
|
||||||
|
})
|
||||||
|
|
||||||
|
mirror.listen(Number(mirrorPort), '127.0.0.1', () => {
|
||||||
|
console.log(`Mirror server listening on http://127.0.0.1:${mirrorPort}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
HOST: host,
|
||||||
|
PORT: port,
|
||||||
|
LNBITS_DATA_FOLDER: dataFolder,
|
||||||
|
LNBITS_ADMIN_UI: process.env.LNBITS_ADMIN_UI || 'true',
|
||||||
|
AUTH_HTTPS_ONLY: process.env.AUTH_HTTPS_ONLY || 'false',
|
||||||
|
LNBITS_BACKEND_WALLET_CLASS:
|
||||||
|
process.env.LNBITS_BACKEND_WALLET_CLASS || 'FakeWallet',
|
||||||
|
LNBITS_EXTENSIONS_DEFAULT_INSTALL:
|
||||||
|
process.env.LNBITS_EXTENSIONS_DEFAULT_INSTALL ||
|
||||||
|
'watchonly,satspay,tipjar,tpos,lnurlp,withdraw'
|
||||||
|
}
|
||||||
|
|
||||||
|
const lnbits = spawn('uv', ['run', 'lnbits', '--host', host, '--port', port], {
|
||||||
|
cwd: root,
|
||||||
|
env,
|
||||||
|
stdio: 'inherit'
|
||||||
|
})
|
||||||
|
|
||||||
|
function shutdown(signal) {
|
||||||
|
mirror.close()
|
||||||
|
if (!lnbits.killed) {
|
||||||
|
lnbits.kill(signal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'))
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
||||||
|
|
||||||
|
lnbits.on('exit', code => {
|
||||||
|
mirror.close()
|
||||||
|
process.exit(code ?? 0)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user