Skip to content

Commit

Permalink
feat(explorer): cluster testing
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfreska committed Feb 24, 2025
1 parent cb4d995 commit f1af007
Show file tree
Hide file tree
Showing 47 changed files with 442 additions and 141 deletions.
2 changes: 1 addition & 1 deletion .github/actions/setup-all/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ inputs:
node_version:
description: Node.js version
required: false
default: '20.10.0'
default: '22.12.0'
go_version:
description: Go version
required: false
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/setup-js/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ inputs:
node_version:
description: Node.js version
required: false
default: '20.10.0'
default: '22.12.0'
base_ref:
description: Determines which base SHA to use ('base' for a specific SHA/branch, 'last_successful' for the last successful workflow)
required: false
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ deploy.log

# Next.js
.next
.next-*

# builds
dist
Expand Down
5 changes: 2 additions & 3 deletions apps/explorer-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { defineConfig, devices } from '@playwright/test'
import { nxE2EPreset } from '@nx/playwright/preset'

import { workspaceRoot } from '@nx/devkit'

// For CI, you may want to set BASE_URL to the deployed application.
Expand All @@ -25,8 +24,8 @@ export default defineConfig({
trace: 'on-first-retry',
video: 'on-first-retry',
},
// Timeout per test.
timeout: 60_000,
// Timeout per test. The cluster takes up to 30 seconds to start and form contracts.
timeout: 180_000,
expect: {
// Raise the timeout because it is running against next dev mode
// which requires compilation the first to a page is visited.
Expand Down
88 changes: 88 additions & 0 deletions apps/explorer-e2e/src/fixtures/cluster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Explored } from '@siafoundation/explored-js'
import { Hostd } from '@siafoundation/hostd-js'
import { Bus } from '@siafoundation/renterd-js'
import { Walletd } from '@siafoundation/walletd-js'
import {
clusterd,
setupCluster,
teardownCluster,
} from '@siafoundation/clusterd'
import { BrowserContext } from 'playwright'

export type Cluster = Awaited<ReturnType<typeof startCluster>>

export async function startCluster({ context }: { context: BrowserContext }) {
const exploredCount = 1
const renterdCount = 1
const walletdCount = 1
const hostdCount = 3
await setupCluster({
exploredCount,
renterdCount,
walletdCount,
hostdCount,
})
const renterd = clusterd.nodes.find((n) => n.type === 'renterd')
const explored = clusterd.nodes.find((n) => n.type === 'explored')
const hostds = clusterd.nodes.filter((n) => n.type === 'hostd')
const walletds = clusterd.nodes.filter((n) => n.type === 'walletd')
if (!renterd || !explored || !hostds || !walletds) {
throw new Error('Failed to start cluster')
}
const daemons = {
renterd,
explored,
hostds,
walletds,
}
const apis = {
renterd: Bus({
api: `${renterd.apiAddress}/api`,
password: renterd.password,
}),
explored: Explored({
api: `${explored.apiAddress}/api`,
password: explored.password,
}),
hostds: hostds.map((h) =>
Hostd({
api: `${h.apiAddress}/api`,
password: h.password,
})
),
walletds: walletds.map((w) =>
Walletd({
api: `${w.apiAddress}/api`,
password: w.password,
})
),
}
console.log(`
clusterd: http://localhost:${clusterd.managementPort}
explored: ${daemons.explored.apiAddress}
renterd: ${daemons.renterd.apiAddress}
hostds: ${daemons.hostds.map((h) => h.apiAddress)}
walletds: ${daemons.walletds.map((w) => w.apiAddress)}
`)
// Set the explorerd address cookie so that the explorer app overrides the
// normal zen address with the testnet cluster address.
await context.addCookies([
{
// This should match `exploredAddressCookieName` in apps/explorer/config/explored.ts
name: 'exploredAddress',
value: daemons.explored.apiAddress,
url: 'http://localhost:3005',
},
])
return {
// webServerUrl: baseUrl,
clusterd,
apis,
daemons,
}
}

export async function stopCluster() {
// stopWebServer()
teardownCluster()
}
9 changes: 0 additions & 9 deletions apps/explorer-e2e/src/fixtures/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,6 @@ export const TEST_TX_1 = {
},
}

export const TEST_ADDRESS_1 = {
id: '68bf48e81536f2221f3809aa9d1c89c1c869a17c6f186a088e49fd2605e4bfaaa24f26e4c42c',
display: {
title: 'Address 68bf48e81536f22...',
transactionNumber: '500 events',
transactionID: 'c0b92135ca06...',
},
}

export const TEST_CONTRACT_1 = {
id: '25c94822bf7bd86a92d28a148d9d30151949f3599bf93af0df7b4f1e1b3c990d',
renewedFromTitle: 'Contract 494d147a8028217...',
Expand Down
3 changes: 3 additions & 0 deletions apps/explorer-e2e/src/fixtures/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function keys<T extends object>(obj: T) {
return Object.keys(obj) as Array<keyof T>
}
96 changes: 96 additions & 0 deletions apps/explorer-e2e/src/fixtures/walletd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Walletd } from '@siafoundation/walletd-js'
import { blake2bHex } from 'blakejs'
import { WalletAddressMetadata } from '@siafoundation/walletd-types'
import { to } from '@siafoundation/request'
import { clusterd, mine } from '@siafoundation/clusterd'
import { Bus } from '@siafoundation/renterd-js'
import { humanSiacoin } from '@siafoundation/units'

export async function addWalletToWalletd(walletd: ReturnType<typeof Walletd>) {
// For some reason when this code runs on GitHub Actions it throws an ESM
// import error for the SDK. Running locally it works fine.
// Error: require() of ES Module sdk/index.esm.js from /fixtures/walletd.ts not supported.
// This dynamic import is a workaround.
const { initSDK, getSDK } = await import('@siafoundation/sdk')
await initSDK()
const sdk = getSDK()
const { phrase: mnemonic } = sdk.wallet.generateSeedPhrase()
const mnemonicHash = blake2bHex(mnemonic)
const [wallet, walletError] = await to(
walletd.walletAdd({
data: {
name: 'test',
description: 'test',
metadata: {
type: 'seed',
mnemonicHash,
},
},
})
)
if (!wallet || walletError) {
throw new Error(`Failed to add wallet: ${walletError}`)
}
const kp = sdk.wallet.keyPairFromSeedPhrase(mnemonic, 0)
const suh = sdk.wallet.standardUnlockHash(kp.publicKey)
const uc = sdk.wallet.standardUnlockConditions(kp.publicKey)
const metadata: WalletAddressMetadata = {
index: 0,
}
const [, addressError] = await to(
walletd.walletAddressAdd({
params: {
id: wallet.id,
},
data: {
address: suh.address,
description: '',
spendPolicy: {
type: 'uc',
policy: uc.unlockConditions,
},
metadata,
},
})
)
if (addressError) {
throw new Error(`Failed to add address: ${addressError}`)
}
return { wallet, address: suh.address }
}

// The renterd node receives all the initial funds so we can use it to fund
// other wallets.
export async function sendSiacoinFromRenterd(address: string, amount: string) {
console.log(`Sending ${humanSiacoin(amount)} from renterd to:`, address)
const renterdNode = clusterd.nodes.find((n) => n.type === 'renterd')
if (!renterdNode) {
throw new Error('Renterd node not found')
}
const bus = Bus({
api: renterdNode.apiAddress + '/api',
password: renterdNode.password,
})

try {
// Send some funds to the wallet.
await bus.walletSend({
data: {
address,
amount,
subtractMinerFee: false,
},
})
await mine(1)
} catch (e) {
console.log('error sending siacoin', e)
}
}

export async function getRenterdAddress() {
const renterdNode = clusterd.nodes.find((n) => n.type === 'renterd')
if (!renterdNode) {
throw new Error('Renterd node not found')
}
return renterdNode.walletAddress
}
89 changes: 63 additions & 26 deletions apps/explorer-e2e/src/specs/address.spec.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,89 @@
import { test, expect } from '@playwright/test'
import { ExplorerApp } from '../fixtures/ExplorerApp'
import { TEST_ADDRESS_1 } from '../fixtures/constants'
import { startCluster, stopCluster } from '../fixtures/cluster'
import { mine, renterdWaitForContracts } from '@siafoundation/clusterd'
import { addWalletToWalletd, sendSiacoinFromRenterd } from '../fixtures/walletd'
import { Cluster } from '../fixtures/cluster'
import { toHastings } from '@siafoundation/units'

let explorerApp: ExplorerApp
let cluster: Cluster

test.beforeEach(async ({ page }) => {
test.beforeEach(async ({ page, context }) => {
cluster = await startCluster({ context })

await renterdWaitForContracts({
renterdNode: cluster.daemons.renterd,
hostdCount: cluster.daemons.hostds.length,
})
explorerApp = new ExplorerApp(page)
})

test.afterEach(async () => {
await stopCluster()
})

test('address can be searched by id', async ({ page }) => {
const wallet = await cluster.apis.renterd.wallet()
await explorerApp.goTo('/')
await explorerApp.navigateBySearchBar(TEST_ADDRESS_1.id)

await expect(page.getByText(TEST_ADDRESS_1.display.title)).toBeVisible()
await explorerApp.navigateBySearchBar(wallet.data.address)
await expect(
page
.getByTestId('entity-heading')
.getByText(`Address ${wallet.data.address.slice(0, 5)}`)
).toBeVisible()
})

test('address can be directly navigated to by id', async ({ page }) => {
await explorerApp.goTo('/address/' + TEST_ADDRESS_1.id)

await expect(page.getByText(TEST_ADDRESS_1.display.title)).toBeVisible()
const wallet = await cluster.apis.renterd.wallet()
await explorerApp.goTo('/address/' + wallet.data.address)
await expect(
page
.getByTestId('entity-heading')
.getByText(`Address ${wallet.data.address.slice(0, 5)}`)
).toBeVisible()
})

test('address displays the intended data', async ({ page }) => {
const displayKeys = Object.keys(TEST_ADDRESS_1.display)

await explorerApp.goTo('/address/' + TEST_ADDRESS_1.id)

for (const key of displayKeys) {
const currentProperty = TEST_ADDRESS_1.display[key]
await expect(page.getByText(currentProperty)).toBeVisible()
}
})

test('address can navigate to the unspent outputs list', async ({ page }) => {
await explorerApp.goTo('/address/' + TEST_ADDRESS_1.id)
const { wallet, address } = await addWalletToWalletd(cluster.apis.walletds[0])
await sendSiacoinFromRenterd(address, toHastings(1_000_000).toString())
await mine(10)
const events = await cluster.apis.walletds[0].walletEvents({
params: {
id: wallet.id,
limit: 1_000,
offset: 0,
},
})
const outputs = await cluster.apis.walletds[0].walletOutputsSiacoin({
params: {
id: wallet.id,
},
})
await explorerApp.goTo('/address/' + address)
await expect(page.getByText(`Address ${address.slice(0, 5)}`)).toBeVisible()
await expect(page.getByText(events.data[0].id.slice(0, 5))).toBeVisible()
await expect(page.getByText(`${events.data.length} events`)).toBeVisible()
await page.getByRole('tab').getByText('Unspent outputs').click()

await expect(page.getByText('Siacoin output').first()).toBeVisible()
await expect(
page.getByText(outputs.data.outputs[0].id.slice(0, 5))
).toBeVisible()
})

test('address can navigate through to a transaction', async ({ page }) => {
await explorerApp.goTo('/address/' + TEST_ADDRESS_1.id)
const wallet = await cluster.apis.renterd.wallet()
const events = await cluster.apis.renterd.walletEvents({
params: {
limit: 1,
offset: 0,
},
})
await explorerApp.goTo('/address/' + wallet.data.address)

await page
.locator(
'a[data-testid="entity-link"][href*="b24a7d623b82206cd3363db0c0d41446f106a59227d86ef081d601315dbd8cca"]'
)
.locator(`a[data-testid="entity-link"][href*="${events.data[0].id}"]`)
.click()

await expect(page.getByText('Transaction b24a7d623b82206...')).toBeVisible()
await expect(page.getByText(events.data[0].id.slice(0, 5))).toBeVisible()
})
3 changes: 2 additions & 1 deletion apps/explorer-e2e/src/specs/block.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ test('block displays the intended data', async ({ page }) => {
await explorerApp.goTo('/block/' + TEST_BLOCK_1.height)

for (const key of displayKeys) {
const currentProperty = TEST_BLOCK_1.display[key]
const currentProperty =
TEST_BLOCK_1.display[key as keyof typeof TEST_BLOCK_1.display]
await expect(page.getByText(currentProperty)).toBeVisible()
}
})
3 changes: 2 additions & 1 deletion apps/explorer-e2e/src/specs/contract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
RENEWED_TO_BUTTON,
TEST_CONTRACT_1,
} from '../fixtures/constants'
import { keys } from '../fixtures/utils'

let explorerApp: ExplorerApp

Expand All @@ -26,7 +27,7 @@ test('contract can be directly navigated to', async ({ page }) => {
})

test('contract displays the intended data', async ({ page }) => {
const displayKeys = Object.keys(TEST_CONTRACT_1.display)
const displayKeys = keys(TEST_CONTRACT_1.display)

await explorerApp.goTo('/contract/' + TEST_CONTRACT_1.id)

Expand Down
Loading

0 comments on commit f1af007

Please sign in to comment.