From 4fd4f2ecdb626d3270112f5a815bed69640aacd5 Mon Sep 17 00:00:00 2001 From: Maksym H Date: Mon, 11 May 2026 13:15:54 +0100 Subject: [PATCH] implement 2 methods and add more tests. bump to 0.7.8 --- CHANGELOG.md | 12 ++ CLAUDE.md | 2 +- package.json | 8 +- pnpm-lock.yaml | 62 +++---- src/browser/host-runtime.ts | 17 ++ test/integration.spec.ts | 315 ++++++++++++++++++++++++++++++++++++ test/test-product.ts | 177 +++++++++++++++++++- 7 files changed, 556 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b942c9e..0cb67d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.7.5 + +### Added + +- **`handleCreateTransaction`** — creates transactions for product accounts. Returns the call data for test assertions. +- **`handleAccountCreateProof`** — creates Ring VRF proofs for product accounts. Signs the message with sr25519 for test purposes. +- **Integration tests** — 12 new tests covering theme subscribe, entropy derivation, login/getUserId, resource allocation, feature check, local storage, statement store proof, create transaction, and account create proof. Total: 46 integration tests (up from 34). + +### Changed + +- **Dependencies** — `@novasamatech/host-api`, `host-container`, `product-sdk` → ^0.7.8 (identity fixes, bulletin package). + ## 0.7.4 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 562c744..da33c20 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ When bumping `@novasamatech/*` dependencies: ## Testing - `pnpm test` — export smoke tests (22 tests) -- `pnpm test:integration` — Playwright E2E tests (34+ tests) that exercise the full host-container ↔ product-sdk protocol +- `pnpm test:integration` — Playwright E2E tests (46+ tests) that exercise the full host-container ↔ product-sdk protocol - Always run both after any change - If the test product (`test/test-product.ts`) uses APIs that changed, update it and rebuild via `node test/build-test-product.mjs` diff --git a/package.json b/package.json index 528ff7f..20dd686 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@parity/host-api-test-sdk", - "version": "0.7.4", + "version": "0.7.5", "description": "Lightweight test host for Spektr product E2E testing — embeds dapps with auto-signing dev accounts, no Docker needed", "license": "MIT", "repository": { @@ -42,11 +42,11 @@ } }, "dependencies": { - "@novasamatech/host-api": "^0.7.7" + "@novasamatech/host-api": "^0.7.8" }, "devDependencies": { - "@novasamatech/host-container": "^0.7.7", - "@novasamatech/product-sdk": "^0.7.7", + "@novasamatech/host-container": "^0.7.8", + "@novasamatech/product-sdk": "^0.7.8", "@polkadot/keyring": "^14.0.0", "@polkadot/types": "^16.0.0", "@polkadot/util": "^14.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b092fbf..c6bb4df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,18 +9,18 @@ importers: .: dependencies: '@novasamatech/host-api': - specifier: ^0.7.7 - version: 0.7.7 + specifier: ^0.7.8 + version: 0.7.8 '@playwright/test': specifier: '>=1.0.0' version: 1.58.2 devDependencies: '@novasamatech/host-container': - specifier: ^0.7.7 - version: 0.7.7(esbuild@0.25.12)(rxjs@7.8.2) + specifier: ^0.7.8 + version: 0.7.8(esbuild@0.25.12)(rxjs@7.8.2) '@novasamatech/product-sdk': - specifier: ^0.7.7 - version: 0.7.7(@polkadot/api@16.5.6)(@polkadot/util@14.0.1)(esbuild@0.25.12)(rxjs@7.8.2) + specifier: ^0.7.8 + version: 0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.1)(esbuild@0.25.12)(rxjs@7.8.2) '@polkadot/keyring': specifier: ^14.0.0 version: 14.0.1(@polkadot/util-crypto@14.0.1(@polkadot/util@14.0.1))(@polkadot/util@14.0.1) @@ -232,17 +232,17 @@ packages: resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} engines: {node: '>= 20.19.0'} - '@novasamatech/host-api@0.7.7': - resolution: {integrity: sha512-2aU6ZIZrfYkdRj39PP9+Rsg+hl5KlzjyC7dUsbcV4mepWtyHFYCCkt621Zg3TmCrT4oDJoY107DOxankB4UV3Q==} + '@novasamatech/host-api@0.7.8': + resolution: {integrity: sha512-CR5Im0cn8t04R74FAvbNjyJzVOsbAPl9GRT4RsEaqu5n3DVbwrc70vWVZ0hwVVpyxzVrGgFCwEwSWBRdQGQzBQ==} - '@novasamatech/host-container@0.7.7': - resolution: {integrity: sha512-X6kbEHa3GHDuAD6BKu6fv94Kh/wPX8WZDRnyMqzru83pccPeDyTGaOqqPLPmxyfJp1iZudlaNwBmDfVWNt211g==} + '@novasamatech/host-container@0.7.8': + resolution: {integrity: sha512-dayqLgFteNozQEutnvkfNMy0EvecEIIG26Xw84lmL3luHiVAyShYcxmrMYX+6j9Iu6ZvVxjQcRVrkoRPLIEqjA==} - '@novasamatech/product-sdk@0.7.7': - resolution: {integrity: sha512-Jh3AXmbt1OblniHfzSdaRS4esXEeaP9Gbstv7/wqgAiMPMmJvmMTYFmNVCLWxFrz55P4NwTwhG1V+yeU940Dmw==} + '@novasamatech/product-sdk@0.7.8': + resolution: {integrity: sha512-oTuCJs4v9iig6nLxhfNewzJfcD9/iUjAQm0rSQF8p4G0ryMVQgDlezlW9AQd3AIn80mwwJKLdfa2nToSuqo1+Q==} - '@novasamatech/scale@0.7.7': - resolution: {integrity: sha512-t4qdnp9gsG8B0LoxKUWpOHB3JAy7/jS7n2vUn3ynPJDDBsKgbqg5GKfWD6QCRdDhtilzR6k9H0LpDS5yCLFvog==} + '@novasamatech/scale@0.7.8': + resolution: {integrity: sha512-FidGBX/j3KZ1pLlosjkwnXc+Rw1FeXAIfwqt95qYz9WwEnR4T4ZZJQfjXBm7dXTxFrzywee/YzflTwQghh+oxA==} '@playwright/test@1.58.2': resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} @@ -1308,18 +1308,18 @@ snapshots: '@noble/hashes@2.2.0': {} - '@novasamatech/host-api@0.7.7': + '@novasamatech/host-api@0.7.8': dependencies: - '@novasamatech/scale': 0.7.7 + '@novasamatech/scale': 0.7.8 nanoevents: 9.1.0 nanoid: 5.1.9 neverthrow: 8.2.0 scale-ts: 1.6.1 - '@novasamatech/host-container@0.7.7(esbuild@0.25.12)(rxjs@7.8.2)': + '@novasamatech/host-container@0.7.8(esbuild@0.25.12)(rxjs@7.8.2)': dependencies: '@noble/hashes': 2.2.0 - '@novasamatech/host-api': 0.7.7 + '@novasamatech/host-api': 0.7.8 '@polkadot-api/substrate-client': 0.7.0 nanoid: 5.1.9 neverthrow: 8.2.0 @@ -1331,9 +1331,9 @@ snapshots: - supports-color - utf-8-validate - '@novasamatech/product-sdk@0.7.7(@polkadot/api@16.5.6)(@polkadot/util@14.0.1)(esbuild@0.25.12)(rxjs@7.8.2)': + '@novasamatech/product-sdk@0.7.8(@polkadot/api@16.5.6)(@polkadot/util@14.0.1)(esbuild@0.25.12)(rxjs@7.8.2)': dependencies: - '@novasamatech/host-api': 0.7.7 + '@novasamatech/host-api': 0.7.8 '@polkadot-api/json-rpc-provider-proxy': 0.4.0 '@polkadot/extension-inject': 0.63.1(@polkadot/api@16.5.6)(@polkadot/util@14.0.1) neverthrow: 8.2.0 @@ -1347,7 +1347,7 @@ snapshots: - supports-color - utf-8-validate - '@novasamatech/scale@0.7.7': + '@novasamatech/scale@0.7.8': dependencies: '@polkadot-api/utils': 0.4.0 scale-ts: 1.6.1 @@ -1821,7 +1821,7 @@ snapshots: '@noble/hashes': 1.8.0 '@polkadot/networks': 14.0.3 '@polkadot/util': 14.0.3 - '@polkadot/wasm-crypto': 7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) + '@polkadot/wasm-crypto': 7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) '@polkadot/wasm-util': 7.5.4(@polkadot/util@14.0.3) '@polkadot/x-bigint': 14.0.3 '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) @@ -1863,11 +1863,11 @@ snapshots: '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) tslib: 2.8.1 - '@polkadot/wasm-bridge@7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': + '@polkadot/wasm-bridge@7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': dependencies: '@polkadot/util': 14.0.3 '@polkadot/wasm-util': 7.5.4(@polkadot/util@14.0.3) - '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) + '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) tslib: 2.8.1 '@polkadot/wasm-crypto-asmjs@7.5.4(@polkadot/util@14.0.1)': @@ -1900,14 +1900,14 @@ snapshots: '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) tslib: 2.8.1 - '@polkadot/wasm-crypto-init@7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': + '@polkadot/wasm-crypto-init@7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': dependencies: '@polkadot/util': 14.0.3 - '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) + '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) '@polkadot/wasm-crypto-asmjs': 7.5.4(@polkadot/util@14.0.3) '@polkadot/wasm-crypto-wasm': 7.5.4(@polkadot/util@14.0.3) '@polkadot/wasm-util': 7.5.4(@polkadot/util@14.0.3) - '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) + '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) tslib: 2.8.1 '@polkadot/wasm-crypto-wasm@7.5.4(@polkadot/util@14.0.1)': @@ -1944,15 +1944,15 @@ snapshots: '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) tslib: 2.8.1 - '@polkadot/wasm-crypto@7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': + '@polkadot/wasm-crypto@7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)))': dependencies: '@polkadot/util': 14.0.3 - '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) + '@polkadot/wasm-bridge': 7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) '@polkadot/wasm-crypto-asmjs': 7.5.4(@polkadot/util@14.0.3) - '@polkadot/wasm-crypto-init': 7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) + '@polkadot/wasm-crypto-init': 7.5.4(@polkadot/util@14.0.3)(@polkadot/x-randomvalues@14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1))) '@polkadot/wasm-crypto-wasm': 7.5.4(@polkadot/util@14.0.3) '@polkadot/wasm-util': 7.5.4(@polkadot/util@14.0.3) - '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.1)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) + '@polkadot/x-randomvalues': 14.0.3(@polkadot/util@14.0.3)(@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)) tslib: 2.8.1 '@polkadot/wasm-util@7.5.4(@polkadot/util@14.0.1)': diff --git a/src/browser/host-runtime.ts b/src/browser/host-runtime.ts index edcd036..76b18a8 100644 --- a/src/browser/host-runtime.ts +++ b/src/browser/host-runtime.ts @@ -453,6 +453,23 @@ function setupContainer( return ok({ context, alias }); }); + // Ring VRF proof: real hosts use an actual ring VRF; for test purposes, + // sign the message with the product account's sr25519 key. + container.handleAccountCreateProof((params, { ok }) => { + const [[dotnsId, idx], _ringLocation, message] = params; + const pair = getPairForProductAccount(config, pairs, dotnsId, idx); + const signature = pair ? pair.sign(message) : new Uint8Array(64); + return ok(signature); + }); + + // ── Create transaction (product account) ──────────────────── + + container.handleCreateTransaction((params, { ok }) => { + const [[_dotnsId, _idx], payload] = params; + // For test purposes, return the callData as-is + return ok(payload.callData); + }); + // ── Sign payload (extrinsic) ───────────────────────────────── container.handleSignPayload((params, { ok, err }) => { diff --git a/test/integration.spec.ts b/test/integration.spec.ts index 6923ccd..2bb1ff6 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -1009,3 +1009,318 @@ test.describe('Container recreation resets logs', () => { } }); }); + +// ── Theme ────────────────────────────────────────────────────────── + +test.describe('Theme', () => { + + test('theme subscribe delivers current theme', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + await product.evaluate(() => { + (window as any).__themeSub = window.__TEST_PRODUCT__.subscribeTheme(); + }); + await page.waitForTimeout(100); + + const themes = await product.evaluate(() => window.__TEST_PRODUCT__.getReceivedThemes()); + expect(themes.length).toBeGreaterThanOrEqual(1); + expect(themes[0]).toBe('light'); + + // Host changes theme + await page.evaluate(() => window.__TEST_HOST__.setTheme('dark')); + await page.waitForTimeout(100); + + const updated = await product.evaluate(() => window.__TEST_PRODUCT__.getReceivedThemes()); + expect(updated).toContain('dark'); + + await product.evaluate(() => (window as any).__themeSub.unsubscribe()); + } finally { + await host.close(); + } + }); +}); + +// ── Entropy ──────────────────────────────────────────────────────── + +test.describe('Entropy', () => { + + test('deriveEntropy returns 32-byte deterministic result', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + const keyHex = '0x' + '01'.repeat(16); // 16-byte key + const a = await product.evaluate((k: string) => + window.__TEST_PRODUCT__.deriveEntropy(k), keyHex); + expect(a.ok).toBe(true); + expect(a.entropyHex).toMatch(/^0x[0-9a-f]{64}$/); // 32 bytes + + // Same key → same result (deterministic) + const b = await product.evaluate((k: string) => + window.__TEST_PRODUCT__.deriveEntropy(k), keyHex); + expect(b.entropyHex).toBe(a.entropyHex); + + // Different key → different result + const c = await product.evaluate((k: string) => + window.__TEST_PRODUCT__.deriveEntropy(k), '0x' + '02'.repeat(16)); + expect(c.ok).toBe(true); + expect(c.entropyHex).not.toBe(a.entropyHex); + } finally { + await host.close(); + } + }); +}); + +// ── Login / getUserId ────────────────────────────────────────────── + +test.describe('Login and user identity', () => { + + test('requestLogin returns success when authenticated', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + const result = await product.evaluate(() => + window.__TEST_PRODUCT__.requestLogin('test')); + expect(result.ok).toBe(true); + expect(result.loginResult).toBe('alreadyConnected'); + } finally { + await host.close(); + } + }); + + test('requestLogin returns rejected when login behavior is reject', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + await page.evaluate(() => window.__TEST_HOST__.simulateDisconnect()); + await page.evaluate(() => window.__TEST_HOST__.setLoginBehavior('reject')); + + const result = await product.evaluate(() => + window.__TEST_PRODUCT__.requestLogin('please')); + expect(result.ok).toBe(true); + expect(result.loginResult).toBe('rejected'); + } finally { + await host.close(); + } + }); + + test('getUserId returns primaryUsername', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + const result = await product.evaluate(() => + window.__TEST_PRODUCT__.getUserId()); + expect(result.ok).toBe(true); + expect(result.primaryUsername).toBe('Alice'); + } finally { + await host.close(); + } + }); +}); + +// ── Resource allocation ──────────────────────────────────────────── + +test.describe('Resource allocation', () => { + + test('all resources are allocated', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + const result = await product.evaluate(() => + window.__TEST_PRODUCT__.requestResourceAllocation([ + { tag: 'StatementStoreAllowance', value: undefined }, + { tag: 'BulletInAllowance', value: undefined }, + { tag: 'SmartContractAllowance', value: 0 }, + { tag: 'AutoSigning', value: undefined }, + ])); + expect(result.ok).toBe(true); + expect(result.outcomes).toHaveLength(4); + expect(result.outcomes!.every(o => o.tag === 'Allocated')).toBe(true); + } finally { + await host.close(); + } + }); +}); + +// ── Feature check ────────────────────────────────────────────────── + +test.describe('Feature check', () => { + + test('chain feature returns true for configured genesis', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + // Default chain is PASEO_ASSET_HUB + const result = await product.evaluate(() => + window.__TEST_PRODUCT__.featureSupported('Chain', + '0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2')); + expect(result.ok).toBe(true); + expect(result.supported).toBe(true); + } finally { + await host.close(); + } + }); + + test('chain feature returns false for unknown genesis', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + const result = await product.evaluate(() => + window.__TEST_PRODUCT__.featureSupported('Chain', '0x' + '00'.repeat(32))); + expect(result.ok).toBe(true); + expect(result.supported).toBe(false); + } finally { + await host.close(); + } + }); +}); + +// ── Local storage ────────────────────────────────────────────────── + +test.describe('Local storage', () => { + + test('write, read, and clear', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + // Write + const w = await product.evaluate(() => + window.__TEST_PRODUCT__.localStorageWrite('test-key', 'hello')); + expect(w.ok).toBe(true); + + // Read back + const r = await product.evaluate(() => + window.__TEST_PRODUCT__.localStorageRead('test-key')); + expect(r.ok).toBe(true); + expect(r.value).toBe('hello'); + + // Clear + const c = await product.evaluate(() => + window.__TEST_PRODUCT__.localStorageClear('test-key')); + expect(c.ok).toBe(true); + + // Read again — should be null + const r2 = await product.evaluate(() => + window.__TEST_PRODUCT__.localStorageRead('test-key')); + expect(r2.ok).toBe(true); + expect(r2.value).toBeNull(); + } finally { + await host.close(); + } + }); +}); + +// ── Statement store proof ────────────────────────────────────────── + +test.describe('Statement store proof', () => { + + test('createProof returns a valid proof', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + productAccounts: { 'test-product.dot/0': 'alice' }, + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + const result = await product.evaluate(() => + window.__TEST_PRODUCT__.statementCreateProof('test-product.dot', 0, '0xdeadbeef')); + expect(result.ok).toBe(true); + expect(result.proof).toBeTruthy(); + } finally { + await host.close(); + } + }); +}); + +// ── Create transaction ───────────────────────────────────────────── + +test.describe('Create transaction', () => { + + test('createTransaction returns successfully', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + const result = await product.evaluate(() => + window.__TEST_PRODUCT__.createTransaction('test-product.dot', 0)); + expect(result.ok).toBe(true); + } finally { + await host.close(); + } + }); +}); + +// ── Account create proof ─────────────────────────────────────────── + +test.describe('Account create proof', () => { + + test('accountCreateProof returns proof bytes', async ({ page }) => { + const host = await createTestHostServer({ + productUrl: productServer.url, + accounts: ['alice'], + }); + + try { + const product = await loadHostAndProduct(page, host.url, productServer.url); + + const result = await product.evaluate(() => + window.__TEST_PRODUCT__.accountCreateProof('test-product.dot', 0)); + expect(result.ok).toBe(true); + expect(result.proofHex).toMatch(/^0x[0-9a-f]+$/); + expect(result.proofHex!.length).toBeGreaterThan(10); + } finally { + await host.close(); + } + }); +}); diff --git a/test/test-product.ts b/test/test-product.ts index 0a12ecd..70a67fb 100644 --- a/test/test-product.ts +++ b/test/test-product.ts @@ -48,22 +48,36 @@ declare global { chatCreateRoom(room: { roomId: string; name: string; icon: string }): Promise; chatRegisterBot(bot: { botId: string; name: string; icon: string }): Promise; chatPostTextMessage(roomId: string, text: string): Promise; - /** Subscribes to chat actions and stores received actions in an array. Returns array reference for polling. */ subscribeChatActions(): { unsubscribe(): void }; getReceivedChatActions(): Array; clearReceivedChatActions(): void; preimageSubmit(value: number[]): Promise; preimageLookup(key: string): Promise; statementSubmit(topicsHex: string[], dataHex: string): Promise; + statementCreateProof(dotnsId: string, index: number, dataHex: string): Promise; statementSubscribe(topicsHex: string[]): { unsubscribe(): void }; getReceivedStatements(): Array; clearReceivedStatements(): void; + // v0.7+ additions + subscribeTheme(): { unsubscribe(): void }; + getReceivedThemes(): string[]; + deriveEntropy(keyHex: string): Promise; + requestLogin(reason?: string): Promise; + getUserId(): Promise; + requestResourceAllocation(resources: Array<{ tag: string; value?: unknown }>): Promise }>; + featureSupported(tag: string, value: unknown): Promise; + localStorageWrite(key: string, value: string): Promise; + localStorageRead(key: string): Promise; + localStorageClear(key: string): Promise; + createTransaction(dotnsId: string, index: number): Promise; + accountCreateProof(dotnsId: string, index: number): Promise; }; } } const receivedChatActions: unknown[] = []; const receivedStatements: unknown[] = []; +const receivedThemes: string[] = []; async function init() { const el = document.getElementById('status')!; @@ -359,6 +373,167 @@ async function init() { clearReceivedStatements() { receivedStatements.length = 0; }, + + // ── Statement create proof ──────────────────────────────── + async statementCreateProof(dotnsId: string, index: number, dataHex: string) { + try { + const data = hexToU8a(dataHex); + const statement = { + proof: undefined, + decryptionKey: undefined, + expiry: undefined, + channel: undefined, + topics: [], + data, + }; + const r = await hostApi.statementStoreCreateProof(enumValue('v1', [[dotnsId, index], statement])); + if (r.isOk()) return { ok: true, proof: r.value.value }; + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + // ── Theme ───────────────────────────────────────────────── + subscribeTheme() { + const sub = hostApi.themeSubscribe(enumValue('v1', undefined), (payload: unknown) => { + const p = payload as { tag?: string; value?: unknown }; + if (p?.tag === 'v1') receivedThemes.push(String(p.value)); + }); + return { unsubscribe() { sub.unsubscribe(); } }; + }, + + getReceivedThemes() { + return [...receivedThemes]; + }, + + // ── Entropy ─────────────────────────────────────────────── + async deriveEntropy(keyHex: string) { + try { + const key = hexToU8a(keyHex); + const r = await hostApi.deriveEntropy(enumValue('v1', key)); + if (r.isOk()) return { ok: true, entropyHex: u8aToHex(r.value.value) }; + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + // ── Login / getUserId ───────────────────────────────────── + async requestLogin(reason?: string) { + try { + const r = await hostApi.requestLogin(enumValue('v1', reason)); + if (r.isOk()) return { ok: true, loginResult: String(r.value.value) }; + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + async getUserId() { + try { + const r = await hostApi.getUserId(enumValue('v1', undefined)); + if (r.isOk()) return { ok: true, primaryUsername: r.value.value.primaryUsername }; + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + // ── Resource allocation ─────────────────────────────────── + async requestResourceAllocation(resources: Array<{ tag: string; value?: unknown }>) { + try { + const r = await hostApi.requestResourceAllocation(enumValue('v1', resources)); + if (r.isOk()) { + const outcomes = (r.value.value as Array<{ tag: string }>).map(o => ({ tag: o.tag })); + return { ok: true, outcomes }; + } + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + // ── Feature check ───────────────────────────────────────── + async featureSupported(tag: string, value: unknown) { + try { + const r = await hostApi.featureSupported(enumValue('v1', { tag, value })); + if (r.isOk()) return { ok: true, supported: r.value.value }; + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + // ── Local storage ───────────────────────────────────────── + async localStorageWrite(key: string, value: string) { + try { + const r = await hostApi.localStorageWrite(enumValue('v1', [key, new TextEncoder().encode(value)])); + if (r.isOk()) return { ok: true }; + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + async localStorageRead(key: string) { + try { + const r = await hostApi.localStorageRead(enumValue('v1', key)); + if (r.isOk()) { + const bytes = r.value.value; + return { ok: true, value: bytes ? new TextDecoder().decode(bytes) : null }; + } + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + async localStorageClear(key: string) { + try { + const r = await hostApi.localStorageClear(enumValue('v1', key)); + if (r.isOk()) return { ok: true }; + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + // ── Create transaction ──────────────────────────────────── + async createTransaction(dotnsId: string, index: number) { + try { + const payload = { + version: 1 as const, + signer: null, + callData: '0x0000' as `0x${string}`, + extensions: [] as Array<{ id: string; extra: `0x${string}`; additionalSigned: `0x${string}` }>, + txExtVersion: 0, + context: { metadata: '0x' as `0x${string}`, tokenSymbol: 'PAS', tokenDecimals: 10, bestBlockHeight: 0 }, + }; + const r = await hostApi.createTransaction(enumValue('v1', [[dotnsId, index], payload])); + if (r.isOk()) return { ok: true }; + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, + + // ── Account create proof ────────────────────────────────── + async accountCreateProof(dotnsId: string, index: number) { + try { + const message = new TextEncoder().encode('test-proof'); + const location = { + genesisHash: '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`, + ringRootHash: '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`, + hints: undefined, + }; + const r = await hostApi.accountCreateProof(enumValue('v1', [[dotnsId, index], location, message])); + if (r.isOk()) return { ok: true, proofHex: u8aToHex(r.value.value) }; + return { ok: false, error: extractError(r.error) }; + } catch (err) { + return { ok: false, error: extractError(err) }; + } + }, }; } catch (err) { el.textContent = `error: ${err}`;