LUX_ENV=local|devnet|testnet|mainnet is now the only environment knob. e2e/env.ts
resolves every base URL, RPC endpoint, network ID and chain ID from NETWORKS.yaml
at runtime; playwright.config.ts and helpers.ts derive from it instead of keeping
their own copies. Those copies had already drifted — the config resolved the
testnet explorer to test-explore.lux.network, which is NXDOMAIN, while helpers.ts
used the real explore-test.lux.network. The retired two-axis ENV/LUX_NETWORK
model now fails loudly rather than being half-honoured.
Projects are viewports (mobile 390x844, tablet 834x1194, laptop 1440x900), so
"mobile and desktop" is the same three frames every run. Screenshots go to
~/Desktop/lux-e2e/<env>/<surface>-<viewport>.png on pass, and shot-reporter.ts
copies a failing test's dying state next to them — a failure you cannot look at
is a failure you cannot fix.
50-login.spec.ts proves sign-in for real: lux.cloud -> lux.id -> /dashboard, with
the password from LUX_E2E_PASSWORD only. It refuses the cheap answers, because
each one lies here — /dashboard is an SPA that returns 200 and keeps its URL when
anonymous, and /auth/callback renders "Sign-in failed" at 200. The proof is the
token: a JWT in sessionStorage whose iss is the IdP and whose email is the
account typed, which lux.id/v1/iam/oauth/userinfo accepts (401 bare, 401 for a
junk token). That is the leg that breaks under invalid_client. It also asserts
the handoff is a public PKCE client and carries a negative control so the
positive assertions cannot go vacuous. Verified with a wrong password: red.
Measured while building this, not assumed:
- workers are per-env. Mainnet is CDN-fronted and ran the gate at 4 with
342/342; testnet hits the raw LB and lost 66 cases to throttling at 4 that
all passed at 2.
- `locator('text=/block/i').first()` in two specs resolved to a hidden Radix
menu trigger, so both tracked the nav's collapse rather than the blocks
table. Replaced by one shared assertion on the section AND a block link.
- the token check runs inside the page: page.request.get attaches request
headers to a thrown error, which printed a live bearer token into the log.
- 40-lux-financial carried a plaintext password; it now comes from the
environment.
Co-authored-by: Hanzo Dev <[email protected]>
202 lines
7.9 KiB
TypeScript
202 lines
7.9 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import {
|
|
getUrls, getNetwork, isLocal,
|
|
rpcCall, ethBlockNumber, ethChainId, getNetworkId,
|
|
isBootstrapped, getNodeVersion, getPeers,
|
|
getCurrentValidators, getBlockchains,
|
|
httpStatus,
|
|
} from './helpers'
|
|
|
|
/**
|
|
* Local validator tests — verify a node can run locally and connect to cloud.lux.network bootnode.
|
|
*
|
|
* These tests validate:
|
|
* 1. Local node is running and healthy
|
|
* 2. Node connects to peers via cloud.lux.network bootnode
|
|
* 3. All chains bootstrap successfully
|
|
* 4. Validator can be registered on P-Chain
|
|
*
|
|
* Run with: LUX_ENV=local npx playwright test e2e/07-validator.spec.ts
|
|
*/
|
|
|
|
const urls = getUrls()
|
|
const network = getNetwork()
|
|
|
|
// This suite targets a LOCAL validator node (LUX_ENV=local). Against production the
|
|
// node health/info API is firewalled by design, so there is no local node to
|
|
// probe — skip the whole suite unless running locally.
|
|
test.beforeEach(() => {
|
|
test.skip(!isLocal(), 'local validator suite — run with LUX_ENV=local')
|
|
})
|
|
|
|
// ── Local Node Health ───────────────────────────────────────────────
|
|
test.describe('Local Validator — Node Health', () => {
|
|
test('local node is running', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 10_000)
|
|
console.log(` Local node health: HTTP ${status}`)
|
|
if (status === 0) {
|
|
console.log(` NOTE: Start local node with: make start network=${network.replace('net', '')}`)
|
|
test.skip(true, 'Local node not running')
|
|
}
|
|
// 200 = healthy, 503 = still bootstrapping (acceptable for local)
|
|
expect([200, 503]).toContain(status)
|
|
})
|
|
|
|
test('local node reports healthy', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
let healthy = false
|
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
try {
|
|
const res = await request.get(urls.rpcHealth, { ignoreHTTPSErrors: true, timeout: 10_000 })
|
|
const json = await res.json()
|
|
healthy = json.healthy ?? json.result?.healthy ?? false
|
|
if (healthy) break
|
|
} catch {
|
|
if (attempt < 2) await new Promise(r => setTimeout(r, 1000))
|
|
}
|
|
}
|
|
expect(healthy).toBe(true)
|
|
})
|
|
|
|
test('local node version', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
const ver = await getNodeVersion(request, urls.rpcInfo)
|
|
console.log(` Node version: ${ver}`)
|
|
expect(ver).toBeTruthy()
|
|
expect(ver.toLowerCase()).toContain('lux')
|
|
})
|
|
})
|
|
|
|
// ── Local Node — Chain Bootstrap ────────────────────────────────────
|
|
test.describe('Local Validator — Chain Bootstrap', () => {
|
|
test('P-Chain bootstrapped', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
const ok = await isBootstrapped(request, urls.rpcInfo, 'P')
|
|
expect(ok).toBe(true)
|
|
})
|
|
|
|
test('C-Chain bootstrapped', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
const ok = await isBootstrapped(request, urls.rpcInfo, 'C')
|
|
expect(ok).toBe(true)
|
|
})
|
|
|
|
test('X-Chain bootstrapped', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
const ok = await isBootstrapped(request, urls.rpcInfo, 'X')
|
|
expect(ok).toBe(true)
|
|
})
|
|
|
|
test('C-Chain produces blocks', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
const block = await ethBlockNumber(request, urls.rpc)
|
|
console.log(` C-Chain block: ${block}`)
|
|
expect(block).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('C-Chain has correct chainId', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
const cid = await ethChainId(request, urls.rpc)
|
|
console.log(` C-Chain chainId: ${cid}`)
|
|
// Accept any valid Lux chain ID
|
|
expect([96369, 96368, 96367, 1337]).toContain(cid)
|
|
})
|
|
})
|
|
|
|
// ── Local Node — Peer Connectivity ──────────────────────────────────
|
|
test.describe('Local Validator — Peer Connectivity', () => {
|
|
test('connects to peers', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
try {
|
|
const peers = await getPeers(request, urls.rpcInfo)
|
|
console.log(` Peers: ${peers.length}`)
|
|
// Local 5-node setup should have 4 peers
|
|
expect(peers.length).toBeGreaterThanOrEqual(1)
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
// info.peers blocked at gateway (security) — this is expected
|
|
if (msg.includes('-32601') || msg.includes('not allowed') || msg.includes('403') || msg.includes('Method not found')) {
|
|
console.log(' info.peers blocked at gateway (security) — PASS')
|
|
return
|
|
}
|
|
throw e
|
|
}
|
|
})
|
|
|
|
test('network ID is correct', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
const nid = await getNetworkId(request, urls.rpcInfo)
|
|
console.log(` Network ID: ${nid}`)
|
|
expect(['1', '2', '3']).toContain(nid)
|
|
})
|
|
})
|
|
|
|
// ── Local Node — Validator Registration ─────────────────────────────
|
|
test.describe('Local Validator — P-Chain Validators', () => {
|
|
test('P-Chain has validators', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
const validators = await getCurrentValidators(request, urls.rpcPlatform) as { nodeID: string }[]
|
|
console.log(` Validators: ${validators.length}`)
|
|
expect(validators.length).toBeGreaterThanOrEqual(1)
|
|
})
|
|
|
|
test('app chains are deployed', async ({ request }) => {
|
|
const status = await httpStatus(request, urls.rpcHealth, 5_000)
|
|
if (status === 0) test.skip(true, 'Local node not running')
|
|
|
|
const chains = await getBlockchains(request, urls.rpcPlatform) as { name: string }[]
|
|
const names = chains.map(c => c.name)
|
|
console.log(` Chains: ${names.join(', ')}`)
|
|
// At minimum P, C, X should be registered
|
|
expect(chains.length).toBeGreaterThanOrEqual(3)
|
|
})
|
|
})
|
|
|
|
// ── Cloud Bootnode — Connectivity ───────────────────────────────────
|
|
test.describe('Cloud Bootnode — cloud.lux.network', () => {
|
|
test('cloud.lux.network is reachable', async ({ request }) => {
|
|
const status = await httpStatus(request, 'https://cloud.lux.network')
|
|
console.log(` cloud.lux.network: HTTP ${status}`)
|
|
expect(status).toBeGreaterThan(0)
|
|
expect(status).toBeLessThan(500)
|
|
})
|
|
|
|
test('cloud bootnode RPC accessible', async ({ request }) => {
|
|
// cloud.lux.network should expose RPC for bootstrap
|
|
for (const path of [
|
|
'/v1/health',
|
|
'/v1/info',
|
|
'/api/health',
|
|
'/health',
|
|
]) {
|
|
const status = await httpStatus(request, `https://cloud.lux.network${path}`, 10_000)
|
|
if (status > 0 && status < 500) {
|
|
console.log(` Bootnode ${path}: HTTP ${status}`)
|
|
return
|
|
}
|
|
}
|
|
// Bootnode may not expose health endpoints directly, frontend is enough
|
|
console.log(' Bootnode health endpoints not directly exposed (fronted by UI)')
|
|
})
|
|
})
|