diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..232e6fc --- /dev/null +++ b/TESTING.md @@ -0,0 +1,21 @@ +# Pengujian (Testing) — OPEN-ISP + +## 1. Uji manual cepat + +1. **Landing** `/` — hero, paket, switcher **ID/EN**, link GitHub/Wiki/Lisensi. +2. **Login admin** `/admin/login` → `admin` / `demo-isp123` → dashboard. +3. **Pelanggan** `/customer/login` → masukkan nomor HP demo **`081200000001`** (login pelanggan hanya butuh nomor HP) → dashboard & tagihan. +4. **Health** `/health` · versi `/version.txt`. + +## 2. E2E otomatis + +Framework **tester-army/e2e** (`@e2e-dev/web`), locator-based tanpa AI. +Suite + cara jalan: lihat **[`tests/e2e/README.md`](tests/e2e/README.md)**. + +```bash +npm i -D e2e@^0.18 @e2e-dev/web +cp tests/e2e/e2e.config.example.ts e2e.config.ts +APP_URL=http://localhost:3001 E2E_TELEMETRY_DISABLED=1 npm run test:e2e +``` + +Cakupan: landing & halaman publik, guard admin, login admin, **sweep 13 halaman admin**, login pelanggan, 404, `/version.txt`. diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 0000000..9f4576e --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,24 @@ +# E2E — OPEN-ISP (tester-army/e2e) + +Suite **locator-based** (tanpa langkah AI/model). 15 tes: publik, guard admin, sweep 13 halaman admin, login pelanggan, 404, versi. + +## Setup + +```bash +npm i -D e2e@^0.18 @e2e-dev/web +cp tests/e2e/e2e.config.example.ts e2e.config.ts +``` + +## Jalankan (aplikasi harus jalan) + +```bash +APP_URL=http://localhost:3001 E2E_TELEMETRY_DISABLED=1 npm run test:e2e +``` + +Kredensial via env: `ADMIN_USER`, `ADMIN_PASS`, `CUSTOMER_PHONE`. + +## Catatan + +- **Login pelanggan cukup nomor HP** (tanpa password) — `081200000001` untuk data demo. +- Suite **mengonsolidasikan login** (login sekali → sweep halaman) agar tidak kena rate-limit login (30 / 15 menit). +- Bug yang ditemukan suite ini: tabel `discount_logs` tidak dibuat saat fresh-install (500 di `/admin/discounts`) — sudah diperbaiki di `config/database.js`. diff --git a/tests/e2e/e2e.config.example.ts b/tests/e2e/e2e.config.example.ts new file mode 100644 index 0000000..a872b5f --- /dev/null +++ b/tests/e2e/e2e.config.example.ts @@ -0,0 +1,21 @@ +import type { E2EConfig } from 'e2e'; +import { web } from '@e2e-dev/web'; +import { gateway } from 'ai'; + +export default { + // The Vercel AI Gateway serves the model id and reads AI_GATEWAY_API_KEY, or the OIDC token of a linked Vercel project. + agents: { + default: { + model: gateway('openai/gpt-6-luna-fast'), + system: 'You are a thorough QA agent. Verify every outcome.', + }, + }, + targets: [{ + engine: web(), + app: { + url: process.env.APP_URL ?? 'http://localhost:3000', + // Or let the runner start the dev server: + // command: { executable: 'npm', args: ['run', 'dev'] }, + }, + }], +} satisfies E2EConfig; diff --git a/tests/e2e/open-isp.e2e.ts b/tests/e2e/open-isp.e2e.ts new file mode 100644 index 0000000..584c96f --- /dev/null +++ b/tests/e2e/open-isp.e2e.ts @@ -0,0 +1,126 @@ +import { test } from '@e2e-dev/web'; +import { expect } from 'e2e'; + +/** + * OPEN-ISP — E2E suite lengkap (locator murni, tanpa langkah AI). + * Target: container lokal (APP_URL=http://localhost:3001). + * Dikonsolidasikan agar minim login (hindari rate-limit login: 30 / 15 menit). + */ + +const ADMIN_USER = process.env.ADMIN_USER ?? 'admin'; +const ADMIN_PASS = process.env.ADMIN_PASS ?? 'demo-isp123'; +const CUSTOMER_PHONE = process.env.CUSTOMER_PHONE ?? '081200000001'; +const ERR_RE = /Internal Server Error|Kesalahan Server|no such table|Cannot read propert|ReferenceError/i; + +async function loginAdmin(app: any, browser: any) { + await app.open('/admin/login'); + await browser.locator('input[name="username"], input[name="user"]').first().fill(ADMIN_USER); + await browser.locator('input[type="password"]').first().fill(ADMIN_PASS); + await browser.locator('button[type="submit"], form button').first().click(); + await expect(browser.locator('body')).toContainText(/Dashboard|Pelanggan|Selamat|Admin/i); +} + +async function checkPage(app: any, browser: any, path: string, pattern: RegExp) { + await app.open(path); + await expect(browser.locator('body')).toBeVisible(); + await expect(browser.locator('body')).toContainText(pattern); + await expect(browser.locator('body')).not.toContainText(ERR_RE); +} + +/* ============ 1. PUBLIK (tanpa login) ============ */ +test('landing page terbuka', async ({ app, browser }) => { + await app.open('/'); + await expect(browser.locator('body')).toBeVisible(); + await expect(browser.locator('h1')).toContainText(/Internet/i); + await expect(browser.locator('body')).toContainText(/OPEN-ISP/i); +}); + +test('halaman login admin tampil', async ({ app, browser }) => { + await app.open('/admin/login'); + await expect(browser.locator('input[type="password"]').first()).toBeVisible(); +}); + +test('halaman login pelanggan tampil', async ({ app, browser }) => { + await app.open('/customer/login'); + await expect(browser.locator('body')).toContainText(/Portal Pelanggan|Masuk|Selamat/i); +}); + +test('halaman registrasi pelanggan tampil', async ({ app, browser }) => { + await checkPage(app, browser, '/customer/register', /Daftar|Register|Nama/i); +}); + +test('halaman cek tagihan publik tampil', async ({ app, browser }) => { + await checkPage(app, browser, '/customer/check-billing', /Tagihan|Cek|Nomor/i); +}); + +test('halaman isolir tampil', async ({ app, browser }) => { + await app.open('/isolated'); + await expect(browser.locator('body')).toBeVisible(); +}); + +test('halaman 404 tampil dengan benar', async ({ app, browser }) => { + await app.open('/halaman-tidak-ada-xyz-123'); + await expect(browser.locator('body')).toContainText(/404|Not Found|Tidak Ditemukan/i); +}); + +test('version.txt memuat billing 0.5', async ({ app, browser }) => { + await app.open('/version.txt'); + await expect(browser.locator('body')).toContainText(/billing\s*=\s*0\.5/); +}); + +/* ============ 2. GUARD ============ */ +test('halaman admin tanpa login diarahkan ke login', async ({ app, browser }) => { + await app.open('/admin/customers'); + await expect(browser.locator('body')).toContainText(/Masuk|Login|Admin/i); +}); + +/* ============ 3. ADMIN (1x login → sweep) ============ */ +test('admin bisa login', async ({ app, browser }) => { + await loginAdmin(app, browser); +}); + +test('admin: sweep halaman utama (login sekali)', async ({ app, browser }) => { + await loginAdmin(app, browser); + const pages: [string, RegExp][] = [ + ['/admin', /Dashboard|Pelanggan|Pendapatan|Tagihan/i], + ['/admin/customers', /Pelanggan/i], + ['/admin/packages', /Paket/i], + ['/admin/billing', /Tagihan|Invoice|Billing/i], + ['/admin/olts', /OLT/i], + ['/admin/settings', /Pengaturan|Settings/i], + ['/admin/monitoring', /Monitoring|Metrik|CPU/i], + ['/admin/mikrotik', /MikroTik|PPPoE/i], + ['/admin/collectors', /Kolektor/i], + ['/admin/discounts', /Diskon/i], + ['/admin/tickets', /Tiket|Ticket/i], + ['/admin/whatsapp', /WhatsApp|Gateway/i], + ['/admin/sidebar-settings', /Sidebar|Aktivasi|Menu/i], + ]; + for (const [path, re] of pages) { + await checkPage(app, browser, path, re); + } +}); + +test('admin: password salah ditolak', async ({ app, browser }) => { + await app.open('/admin/login'); + await browser.locator('input[name="username"]').first().fill(ADMIN_USER); + await browser.locator('input[type="password"]').first().fill('password-salah-xyz'); + await browser.locator('button[type="submit"], form button').first().click(); + await expect(browser.locator('body')).toContainText(/Masuk|Login|salah|invalid|gagal/i); +}); + +/* ============ 4. PORTAL PELANGGAN (1x login) ============ */ +test('pelanggan: login (nomor HP) → dashboard → tagihan', async ({ app, browser }) => { + await app.open('/customer/login'); + await browser.locator('input[name="phone"], input[name="login"], input[type="text"]').first().fill(CUSTOMER_PHONE); + await browser.locator('button[type="submit"], form button').first().click(); + await expect(browser.locator('body')).toContainText(/Selamat datang|Beranda|Tagihan|Dasbor|Mbps/i); + await checkPage(app, browser, '/customer/dashboard', /Mbps|Paket|Masa Aktif|Tagihan/i); +}); + +/* ============ 5. LOKALISASI ============ */ +test('switcher bahasa EN mengubah landing', async ({ app, browser }) => { + await app.open('/lang/en'); + await app.open('/'); + await expect(browser.locator('body')).toBeVisible(); +});