Skip to content

Commit 0d5b01b

Browse files
authored
Merge pull request #27 from tokenhost/feat/job-board-canonical-integration
Integration tests: assert canonical Job Board functionality in CI
2 parents 0d7b964 + 79ca9c4 commit 0d5b01b

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { expect } from 'chai';
2+
import fs from 'fs';
3+
import os from 'os';
4+
import path from 'path';
5+
import { spawn, spawnSync } from 'child_process';
6+
import Web3 from 'web3';
7+
8+
function runTh(args, cwd) {
9+
return spawnSync('node', [path.resolve('packages/cli/dist/index.js'), ...args], {
10+
cwd,
11+
encoding: 'utf-8'
12+
});
13+
}
14+
15+
function hasAnvil() {
16+
const res = spawnSync('anvil', ['--version'], { encoding: 'utf-8' });
17+
if (res.error && res.error.code === 'ENOENT') return false;
18+
return res.status === 0;
19+
}
20+
21+
function waitForOutput(proc, pattern, timeoutMs) {
22+
return new Promise((resolve, reject) => {
23+
const startedAt = Date.now();
24+
let combined = '';
25+
let done = false;
26+
27+
function cleanup() {
28+
if (done) return;
29+
done = true;
30+
clearInterval(timer);
31+
proc.stdout?.off('data', onData);
32+
proc.stderr?.off('data', onData);
33+
}
34+
35+
function onData(chunk) {
36+
combined += String(chunk ?? '');
37+
if (pattern.test(combined)) {
38+
cleanup();
39+
resolve(combined);
40+
}
41+
}
42+
43+
proc.stdout?.on('data', onData);
44+
proc.stderr?.on('data', onData);
45+
46+
const timer = setInterval(() => {
47+
if (Date.now() - startedAt < timeoutMs) return;
48+
cleanup();
49+
reject(new Error(`Timed out waiting for output match: ${pattern}\nOutput:\n${combined}`));
50+
}, 200);
51+
});
52+
}
53+
54+
async function request(url, init) {
55+
const res = await fetch(url, init);
56+
const text = await res.text();
57+
let json = null;
58+
try {
59+
json = text ? JSON.parse(text) : null;
60+
} catch {
61+
json = null;
62+
}
63+
return { status: res.status, json, text };
64+
}
65+
66+
describe('Job Board canonical app integration', function () {
67+
it('validates end-to-end behavior for build + preview + contract CRUD/payment paths', async function () {
68+
this.timeout(240000);
69+
if (!hasAnvil()) this.skip();
70+
71+
const schemaPath = path.join(process.cwd(), 'apps', 'example', 'job-board.schema.json');
72+
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'th-job-board-'));
73+
74+
const buildRes = runTh(['build', schemaPath, '--out', outDir], process.cwd());
75+
expect(buildRes.status, buildRes.stderr || buildRes.stdout).to.equal(0);
76+
77+
const port = 44000 + Math.floor(Math.random() * 1000);
78+
const host = '127.0.0.1';
79+
const baseUrl = `http://${host}:${port}`;
80+
81+
const preview = spawn(
82+
'node',
83+
[path.resolve('packages/cli/dist/index.js'), 'preview', outDir, '--host', host, '--port', String(port)],
84+
{ cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] }
85+
);
86+
87+
try {
88+
await waitForOutput(preview, new RegExp(`http://${host}:${port}/`), 90000);
89+
90+
const manifestRes = await request(`${baseUrl}/.well-known/tokenhost/manifest.json`);
91+
expect(manifestRes.status).to.equal(200);
92+
93+
const deployment = manifestRes.json?.deployments?.find((d) => d?.role === 'primary') ?? manifestRes.json?.deployments?.[0];
94+
expect(deployment?.deploymentEntrypointAddress).to.match(/^0x[0-9a-fA-F]{40}$/);
95+
const appAddress = deployment.deploymentEntrypointAddress;
96+
expect(String(appAddress).toLowerCase()).to.not.equal('0x0000000000000000000000000000000000000000');
97+
98+
// Route health checks for canonical UI routes.
99+
for (const route of ['/', '/Candidate/', '/Candidate/new/', '/JobPosting/', '/JobPosting/new/']) {
100+
const routeRes = await request(`${baseUrl}${route}`);
101+
expect(routeRes.status, `route ${route} should return 200`).to.equal(200);
102+
}
103+
104+
const compiled = JSON.parse(fs.readFileSync(path.join(outDir, 'compiled', 'App.json'), 'utf-8'));
105+
const abi = compiled.abi;
106+
expect(Array.isArray(abi)).to.equal(true);
107+
108+
const web3 = new Web3('http://127.0.0.1:8545');
109+
const account = web3.eth.accounts.privateKeyToAccount(
110+
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
111+
);
112+
web3.eth.accounts.wallet.add(account);
113+
web3.eth.defaultAccount = account.address;
114+
115+
const app = new web3.eth.Contract(abi, appAddress);
116+
117+
// Candidate create + list + get + update + delete
118+
await app.methods
119+
.createCandidate('alice', 'initial bio', 'https://example.com/alice.png')
120+
.send({ from: account.address, gas: 3_000_000 });
121+
122+
const candidateIds = await app.methods.listIdsCandidate(0, 10, false).call();
123+
expect(Array.isArray(candidateIds)).to.equal(true);
124+
expect(candidateIds.length).to.be.greaterThan(0);
125+
expect(String(candidateIds[0])).to.equal('1');
126+
127+
const candidateGet = await app.methods['getCandidate(uint256)'](1).call();
128+
expect(candidateGet.handle).to.equal('alice');
129+
expect(candidateGet.bio).to.equal('initial bio');
130+
131+
await app.methods
132+
.updateCandidate(1, 'updated bio', 'https://example.com/alice-new.png')
133+
.send({ from: account.address, gas: 3_000_000 });
134+
const candidateUpdated = await app.methods['getCandidate(uint256)'](1).call();
135+
expect(candidateUpdated.bio).to.equal('updated bio');
136+
137+
await app.methods.deleteCandidate(1).send({ from: account.address, gas: 3_000_000 });
138+
const candidateWithDeleted = await app.methods['getCandidate(uint256,bool)'](1, true).call();
139+
expect(Boolean(candidateWithDeleted.isDeleted)).to.equal(true);
140+
const candidateActiveIds = await app.methods.listIdsCandidate(0, 10, false).call();
141+
expect(candidateActiveIds.length).to.equal(0);
142+
143+
// JobPosting paid creates: fail without value, then succeed with required payment.
144+
let unpaidCreateFailed = false;
145+
try {
146+
await app.methods.createJobPosting('Engineer', 'Remote role', '150000').send({
147+
from: account.address,
148+
gas: 3_000_000
149+
});
150+
} catch {
151+
unpaidCreateFailed = true;
152+
}
153+
expect(unpaidCreateFailed).to.equal(true);
154+
155+
await app.methods.createJobPosting('Engineer', 'Remote role', '150000').send({
156+
from: account.address,
157+
gas: 3_000_000,
158+
value: '10000000000000000'
159+
});
160+
161+
const jobIds = await app.methods.listIdsJobPosting(0, 10, false).call();
162+
expect(Array.isArray(jobIds)).to.equal(true);
163+
expect(jobIds.length).to.equal(1);
164+
165+
const job = await app.methods['getJobPosting(uint256)'](1).call();
166+
expect(job.title).to.equal('Engineer');
167+
168+
await app.methods.updateJobPosting(1, 'Updated desc', '175000').send({
169+
from: account.address,
170+
gas: 3_000_000
171+
});
172+
const jobUpdated = await app.methods['getJobPosting(uint256)'](1).call();
173+
expect(jobUpdated.description).to.equal('Updated desc');
174+
175+
await app.methods.deleteJobPosting(1).send({ from: account.address, gas: 3_000_000 });
176+
const jobWithDeleted = await app.methods['getJobPosting(uint256,bool)'](1, true).call();
177+
expect(Boolean(jobWithDeleted.isDeleted)).to.equal(true);
178+
} finally {
179+
preview.kill('SIGINT');
180+
}
181+
});
182+
});

0 commit comments

Comments
 (0)