Skip to content

Commit 5ea56cb

Browse files
authored
Merge pull request #22 from ethstorage/test
Add GoE test scripts covering core commands and Git operations
2 parents 2ce0338 + 7780ed4 commit 5ea56cb

5 files changed

Lines changed: 377 additions & 0 deletions

File tree

.github/workflows/wallet-ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: goe-wallet-ci
2+
3+
on:
4+
push:
5+
branches:
6+
- 'test'
7+
workflow_dispatch:
8+
9+
jobs:
10+
wallet-create:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-node@v4
17+
with:
18+
node-version: 20
19+
cache: 'yarn'
20+
21+
- run: yarn install
22+
23+
- run: yarn build
24+
25+
- name: Run Wallet Create CI Test
26+
run: node test/createWalletCI.mjs

src/cli/wallet/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import readlineSync from 'readline-sync';
22

33
export function promptPassword(message: string): string {
4+
if (process.env.GOE_TEST_MODE === '1') {
5+
const pwd = process.env.GOE_TEST_PASSWORD;
6+
if (!pwd) {
7+
throw new Error('GOE_TEST_PASSWORD not set');
8+
}
9+
return pwd;
10+
}
11+
412
return readlineSync.question(message, {
513
hideEchoBack: true,
614
mask: '*'

test/createWalletCI.mjs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import path from 'path';
2+
import { runCommand } from "./runCommand.mjs";
3+
4+
const DIST_CLI = path.resolve('./dist/cli/index.js');
5+
const PASSWORD = '12345678';
6+
7+
(async () => {
8+
try {
9+
const output = await runCommand(
10+
'node',
11+
[DIST_CLI, 'wallet', 'create'],
12+
{
13+
env: {
14+
GOE_TEST_MODE: '1',
15+
GOE_TEST_PASSWORD: PASSWORD,
16+
},
17+
capture: true
18+
}
19+
);
20+
if (!output.includes('Wallet Address:')) {
21+
throw new Error('Wallet creation failed: no address found');
22+
}
23+
24+
console.log('Unlocking wallet using GOE_TEST_PASSWORD...');
25+
await runCommand(
26+
'node',
27+
[DIST_CLI, 'wallet', 'unlock'],
28+
{
29+
env: {
30+
GOE_TEST_MODE: '1',
31+
GOE_TEST_PASSWORD: PASSWORD,
32+
},
33+
}
34+
);
35+
36+
await runCommand('node', [DIST_CLI, 'wallet', 'lock']);
37+
38+
console.log('✅ Wallet create + unlock/lock test passed!');
39+
} catch (e) {
40+
console.error('❌ Wallet create test failed:', e.message);
41+
process.exit(1);
42+
}
43+
})();

test/runCommand.mjs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* -------- exec: capture stdout -------- */
2+
import {spawn} from "child_process";
3+
4+
const SHELL_CMDS = new Set([
5+
'npm',
6+
'pnpm',
7+
'yarn',
8+
'npx',
9+
]);
10+
11+
12+
export function runCommand(cmd, args = [], options = {}) {
13+
const {
14+
capture = false,
15+
env = {},
16+
cwd,
17+
} = options;
18+
19+
const useShell = SHELL_CMDS.has(cmd);
20+
return new Promise((resolve, reject) => {
21+
const child = spawn(cmd, args, {
22+
shell: useShell, // ⭐
23+
stdio: capture ? 'pipe' : 'inherit',
24+
env: { ...process.env, ...env },
25+
cwd,
26+
});
27+
28+
let stdout = '';
29+
let stderr = '';
30+
31+
if (capture) {
32+
child.stdout.on('data', d => (stdout += d));
33+
child.stderr.on('data', d => (stderr += d));
34+
}
35+
36+
child.on('error', reject);
37+
38+
child.on('close', (code) => {
39+
if (code !== 0) {
40+
return reject(
41+
new Error(
42+
stderr.trim() || `Command failed: ${cmd} ${args.join(' ')}`
43+
)
44+
);
45+
}
46+
resolve(capture ? stdout : true);
47+
});
48+
});
49+
}

test/test.mjs

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// test/goeE2ETest.mjs
2+
import 'dotenv/config';
3+
import path from 'path';
4+
import fs from 'fs';
5+
import { fileURLToPath } from 'url';
6+
import { runCommand } from "./runCommand.mjs";
7+
8+
const DIST_CLI = path.resolve('./dist/cli/index.js');
9+
const CHAIN_ID = '11155111';
10+
11+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
12+
const PROJECT_ROOT = path.resolve(__dirname, '../test');
13+
const TEMP_DIR = path.join(PROJECT_ROOT, '/.tmp');
14+
15+
const PASSWORD = process.env.GOE_TEST_PASSWORD;
16+
17+
if (!PASSWORD) {
18+
console.error('Please set "GOE_TEST_PASSWORD" in .env');
19+
process.exit(1);
20+
}
21+
22+
23+
24+
/* ---------------- utils ---------------- */
25+
26+
const randomRepoName = () =>
27+
`goe-e2e-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
28+
29+
const stripAnsi = (s) =>
30+
s.replace(/\x1B\[[0-9;]*m/g, '');
31+
32+
33+
/* ------------------- Wallet ------------------- */
34+
async function checkWalletExists() {
35+
const out = await runCommand('node', [DIST_CLI, 'wallet', 'list'], { capture: true });
36+
if (out.includes('No wallets found')) {
37+
throw new Error('No wallet exists. Create and fund one first.');
38+
}
39+
}
40+
41+
async function unlockWallet() {
42+
console.log('Unlocking wallet using GOE_TEST_PASSWORD...');
43+
await runCommand(
44+
'node',
45+
[DIST_CLI, 'wallet', 'unlock'],
46+
{
47+
env: {
48+
GOE_TEST_MODE: '1',
49+
GOE_TEST_PASSWORD: PASSWORD,
50+
},
51+
}
52+
);
53+
}
54+
55+
const lockWallet = () =>
56+
runCommand('node', [DIST_CLI, 'wallet', 'lock']);
57+
58+
/* ------------------- Repo ------------------- */
59+
async function createRepo() {
60+
const name = randomRepoName();
61+
const out = await runCommand(
62+
'node',
63+
[DIST_CLI, 'repo', 'create', name, '--chain-id', CHAIN_ID],
64+
{ capture: true }
65+
);
66+
const match = stripAnsi(out).match(/0x[a-fA-F0-9]{40}/);
67+
if (!match) {
68+
throw new Error('Repo address not found');
69+
}
70+
71+
return { name, address: match[0] };
72+
}
73+
74+
const listRepos = () =>
75+
runCommand('node', [DIST_CLI, 'repo', 'list', '--chain-id', CHAIN_ID]);
76+
77+
const listBranches = (addr) =>
78+
runCommand('node', [DIST_CLI, 'repo', 'branches', addr, '--chain-id', CHAIN_ID],);
79+
80+
const setDefaultBranch = (addr) =>
81+
runCommand('node', [DIST_CLI, 'repo', 'default-branch', addr, 'main', '--chain-id', CHAIN_ID],);
82+
83+
/* ------------------- Git flow ------------------- */
84+
async function gitFlow(goe) {
85+
const repoPath = path.join(TEMP_DIR, 'flow');
86+
fs.mkdirSync(repoPath, { recursive: true });
87+
process.chdir(repoPath);
88+
89+
// init + remote
90+
await runCommand('git', ['init']);
91+
await runCommand('git', ['remote', 'add', 'origin', goe]);
92+
await runCommand('git', ['checkout', '-b', 'main']);
93+
94+
// init commit
95+
fs.writeFileSync('README.md', '# GoE E2E\n');
96+
await runCommand('git', ['add', '.']);
97+
await runCommand('git', ['commit', '-m', 'init']);
98+
await runCommand('git', ['push', 'origin', 'main']);
99+
100+
// feature branch
101+
await runCommand('git', ['checkout', '-b', 'feature']);
102+
fs.writeFileSync('feature.txt', 'feature\n');
103+
await runCommand('git', ['add', '.']);
104+
await runCommand('git', ['commit', '-m', 'feature']);
105+
await runCommand('git', ['push', 'origin', 'feature']);
106+
107+
// force push
108+
fs.writeFileSync('feature.txt', 'force\n');
109+
await runCommand('git', ['add', '.']);
110+
await runCommand('git', ['commit', '-m', 'force-update']);
111+
await runCommand('git', ['push', '--force', 'origin', 'feature']);
112+
113+
// delete branch
114+
await runCommand('git', ['push', 'origin', '--delete', 'feature']);
115+
}
116+
117+
async function gitCloneVerify(goe) {
118+
const clonePath = path.join(TEMP_DIR, 'clone');
119+
fs.mkdirSync(clonePath, { recursive: true });
120+
process.chdir(TEMP_DIR);
121+
122+
await runCommand('git', ['clone', goe, clonePath]);
123+
process.chdir(clonePath);
124+
125+
const readme = fs.readFileSync(path.join(clonePath, 'README.md'), 'utf8');
126+
if (!readme.includes('GoE E2E')) throw new Error('Clone verification failed');
127+
128+
const head = await runCommand('git',['symbolic-ref', 'HEAD'], { capture: true });
129+
if (!head.includes('refs/heads/main')) throw new Error('HEAD is not refs/heads/main');
130+
131+
const branches = await runCommand('git', ['branch', '-r'], { capture: true });
132+
if (branches.includes('feature')) throw new Error('Deleted branch still exists');
133+
}
134+
135+
async function gitRejectNonFastForward(goe) {
136+
const repoPath = path.join(TEMP_DIR, 'nff');
137+
fs.mkdirSync(repoPath, { recursive: true });
138+
process.chdir(repoPath);
139+
140+
await runCommand('git', ['clone', goe, '.']);
141+
142+
// push new file
143+
fs.writeFileSync('nff.txt', '1');
144+
await runCommand('git', ['add', '.']);
145+
await runCommand('git', ['commit', '-m', 'nff-1']);
146+
await runCommand('git', ['push', 'origin', 'main']);
147+
148+
// reset head
149+
await runCommand('git', ['reset', '--hard', 'HEAD~1']);
150+
151+
// non-fast-forward push should fail
152+
try {
153+
await runCommand('git', ['push', 'origin', 'main']);
154+
throw new Error('Expected non-fast-forward push to fail');
155+
} catch {
156+
console.log('[expected] non-fast-forward rejected');
157+
}
158+
}
159+
160+
async function gitMergePush(goe) {
161+
const repoPath = path.join(TEMP_DIR, 'merge');
162+
fs.mkdirSync(repoPath, { recursive: true });
163+
process.chdir(repoPath);
164+
165+
await runCommand('git', ['clone', goe, '.']);
166+
167+
// create a branch and commit
168+
await runCommand('git', ['checkout', '-b', 'merge-a']);
169+
fs.writeFileSync('a.txt', 'a');
170+
await runCommand('git', ['add', 'a.txt']);
171+
await runCommand('git', ['commit', '-m', 'a']);
172+
173+
// merge back to main
174+
await runCommand('git', ['checkout', 'main']);
175+
await runCommand('git', ['merge', 'merge-a']);
176+
177+
await runCommand('git', ['push', 'origin', 'main']);
178+
}
179+
180+
async function gitFetchUpdate(goe) {
181+
const repoPath = path.join(TEMP_DIR, 'fetch');
182+
fs.mkdirSync(repoPath, { recursive: true });
183+
process.chdir(repoPath);
184+
185+
await runCommand('git', ['clone', goe, '.']);
186+
187+
fs.writeFileSync('fetch.txt', 'x');
188+
await runCommand('git', ['add', 'fetch.txt']);
189+
await runCommand('git', ['commit', '-m', 'fetch-update']);
190+
await runCommand('git', ['push', 'origin', 'main']);
191+
192+
// simulate fetch from another clone
193+
const clonePath = path.join(TEMP_DIR, 'fetch-clone');
194+
fs.mkdirSync(clonePath, { recursive: true });
195+
await runCommand('git', ['clone', goe, clonePath]);
196+
process.chdir(clonePath);
197+
198+
await runCommand('git', ['fetch', 'origin']);
199+
const result = await runCommand('git', ['log', 'origin/main', '--oneline', '-1'], { capture: true });
200+
if (!result.includes('fetch-update')) throw new Error('Fetch did not update origin/main');
201+
}
202+
203+
204+
function cleanup() {
205+
process.chdir(PROJECT_ROOT);
206+
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
207+
console.log('Local test data cleaned');
208+
}
209+
210+
/* ------------------- main ------------------- */
211+
async function npmPrepareAndLink() {
212+
console.log('\nPreparing local goe-cli...');
213+
214+
// 1. build
215+
await runCommand('npm', ['run', 'build']);
216+
217+
// 2. link
218+
console.log('\nLinking local goe-cli...');
219+
await runCommand('npm', ['link']);
220+
}
221+
222+
(async () => {
223+
fs.mkdirSync(TEMP_DIR, { recursive: true });
224+
225+
// goe
226+
await npmPrepareAndLink();
227+
await checkWalletExists();
228+
await unlockWallet();
229+
const {name, address} = await createRepo();
230+
await listRepos();
231+
232+
// git helper
233+
const goe = `goe://${name}:${CHAIN_ID}`
234+
try {
235+
await gitFlow(goe);
236+
await gitCloneVerify(goe);
237+
await gitRejectNonFastForward(goe);
238+
await gitMergePush(goe);
239+
await gitFetchUpdate(goe);
240+
} finally {
241+
cleanup();
242+
}
243+
244+
await listBranches(address);
245+
await setDefaultBranch(address);
246+
await lockWallet();
247+
console.log('\n=== GoE E2E test finished successfully ===');
248+
})().catch((e) => {
249+
console.error(e.message);
250+
process.exit(1);
251+
});

0 commit comments

Comments
 (0)