feat: initial IPFS Portal — Next.js frontend for remote IPFS node management

Multi-file upload with Quick Upload + Crypto Payment tabs.
Dashboard with live IPFS metrics, Explorer, Peers, Pins, History, Settings pages.
Docker deploy (standalone output), full test suite (16 Playwright tests).
Payment integration via IPFSPortalPayment + MockUSDC contracts on zkSync.
This commit is contained in:
maikrolf
2026-06-25 19:47:57 +02:00
commit 1ddc89479c
42 changed files with 8383 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
import { test, expect } from '@playwright/test';
test.describe('IPFS Portal', () => {
const BASE = 'http://localhost:3445';
test('health endpoint returns 200', async ({ request }) => {
const r = await request.get(`${BASE}/api/health`);
expect(r.ok()).toBeTruthy();
const body = await r.json();
expect(body).toHaveProperty('status', 'ok');
expect(body).toHaveProperty('kuboApi');
});
test('dashboard loads without errors', async ({ page }) => {
const errors: string[] = [];
page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); });
page.on('pageerror', (err) => errors.push('PAGE: ' + err.message));
await page.goto(`${BASE}/dashboard`, { waitUntil: 'networkidle' });
await expect(page.locator('h1')).toContainText('Dashboard');
const critical = errors.filter(
(e) => !e.includes('favicon') && !e.includes('DevTools') && !e.includes('404'),
);
expect(critical).toHaveLength(0);
});
test('explorer page renders', async ({ page }) => {
await page.goto(`${BASE}/explorer`, { waitUntil: 'networkidle' });
await expect(page.locator('h1')).toContainText(/Explorer/i);
});
test('peers page loads', async ({ page }) => {
await page.goto(`${BASE}/peers`, { waitUntil: 'networkidle' });
await expect(page.locator('h1')).toContainText(/Peers/i);
});
test('pins page loads', async ({ page }) => {
await page.goto(`${BASE}/pins`, { waitUntil: 'networkidle' });
await expect(page.locator('h1')).toContainText(/Pins/i);
});
test('upload page renders with tabs and drop zone', async ({ page }) => {
await page.goto(`${BASE}/upload`, { waitUntil: 'networkidle' });
await expect(page.locator('h1')).toContainText(/Upload/i);
// Quick Upload tab should show drop zone with the highlighted "Click to browse" text
await expect(page.getByText('Click to browse', { exact: true })).toBeVisible();
// Tab switcher should exist
await expect(page.getByRole('button', { name: /Quick Upload/i })).toBeVisible();
await expect(page.getByRole('button', { name: /Crypto Payment/i })).toBeVisible();
});
test('upload page crypto tab switches correctly', async ({ page }) => {
await page.goto(`${BASE}/upload`, { waitUntil: 'networkidle' });
// Switch to Crypto tab
await page.getByRole('button', { name: 'Crypto Payment' }).click();
await expect(page.locator('text=Select a file to see pricing')).toBeVisible();
});
test('IPNS page loads keys', async ({ page }) => {
await page.goto(`${BASE}/ipns`, { waitUntil: 'networkidle' });
await expect(page.locator('h1')).toContainText(/IPNS/i);
});
test('history page loads with empty state', async ({ page }) => {
await page.goto(`${BASE}/history`, { waitUntil: 'networkidle' });
await expect(page.locator('h1')).toContainText(/Upload History/i);
await expect(page.locator('text=No upload history yet')).toBeVisible();
await expect(page.locator('text=Upload your first file')).toBeVisible();
});
test('settings page loads with editable form', async ({ page }) => {
await page.goto(`${BASE}/settings`, { waitUntil: 'networkidle' });
await expect(page.locator('h1')).toContainText('Settings');
// Should show input fields
const inputs = page.locator('input');
const count = await inputs.count();
expect(count).toBeGreaterThanOrEqual(3);
// Save and Reset buttons
await expect(page.locator('button:has-text("Save Settings")')).toBeVisible();
await expect(page.locator('button:has-text("Reset Defaults")')).toBeVisible();
});
test('IPFS node info via proxy', async ({ request }) => {
const r = await request.get(`${BASE}/api/node/info`);
expect(r.ok()).toBeTruthy();
const body = await r.json();
expect(body).toHaveProperty('ID');
expect(body).toHaveProperty('AgentVersion');
});
test('swarm peers endpoint', async ({ request }) => {
const r = await request.get(`${BASE}/api/peers`);
expect(r.ok()).toBeTruthy();
const body = await r.json();
expect(body).toHaveProperty('Peers');
});
test('repo stats endpoint', async ({ request }) => {
const r = await request.get(`${BASE}/api/repo/stats`);
expect(r.ok()).toBeTruthy();
const body = await r.json();
expect(body).toHaveProperty('RepoSize');
expect(body).toHaveProperty('NumObjects');
});
test('sidebar navigation links exist and work', async ({ page }) => {
await page.goto(`${BASE}/dashboard`, { waitUntil: 'networkidle' });
await expect(page.locator('h1')).toContainText('Dashboard');
const sidebar = page.locator('aside');
await expect(sidebar).toBeVisible();
const links = sidebar.getByRole('link');
const count = await links.count();
expect(count).toBeGreaterThanOrEqual(8); // History added
for (const name of ['Explorer', 'Pins', 'Peers', 'History']) {
await sidebar.getByRole('link', { name }).click();
await expect(page).toHaveURL(new RegExp(name.toLowerCase()));
}
});
/* ── Payment Flow ── */
test('upload page crypto tab shows payment panel after file selection', async ({ page }) => {
await page.goto(`${BASE}/upload`, { waitUntil: 'networkidle' });
// Switch to Crypto tab
await page.getByRole('button', { name: 'Crypto Payment' }).click();
// Create a dummy file and set it on the crypto file input
const fileInput = page.locator('input[type="file"]').last();
await fileInput.setInputFiles({
name: 'test.txt',
mimeType: 'text/plain',
buffer: Buffer.from('hello ipfs payment test'),
});
// Payment panel should now be visible with "Crypto Payment" heading
await expect(page.getByRole('heading', { name: 'Crypto Payment' })).toBeVisible();
// Should either show "not deployed" or "Connect Wallet"
const notDeployed = page.locator('text=Payment contract not deployed');
const connectWallet = page.locator('button:has-text("Connect Wallet")');
const isNotDeployed = await notDeployed.isVisible().catch(() => false);
if (isNotDeployed) {
await expect(notDeployed).toBeVisible();
} else {
await expect(connectWallet).toBeVisible();
}
});
test('payment verification API accepts address query', async ({ request }) => {
// Test with a zero address to verify the API structure works
const r = await request.get(`${BASE}/api/payment/verify?address=0x0000000000000000000000000000000000000000`);
expect(r.ok()).toBeTruthy();
const body = await r.json();
// Should either show deployed or not
expect(body).toHaveProperty('deployed');
if (body.deployed) {
expect(body).toHaveProperty('stats');
expect(body.stats).toHaveProperty('totalBytes');
}
});
});