Uji API
Uji API
Section titled “Uji API”Uji alur login dan otorisasi pada aplikasi yang berjalan.
Berkas: alat/hari-3/Praktik 9/tools/uji-api.mjs
#!/usr/bin/env node// =====================================================================// Praktik 9 - uji-api.mjs//// Menguji API CRUD Praktik 9 pada aplikasi yang SEDANG BERJALAN. Praktik 9// hanya memakai Postman secara manual; skrip ini menjalankan urutan yang// sama tetapi bisa diulang dan hasilnya dibandingkan.//// Urutan uji:// 1. login dengan email/password salah -> harus 401// 2. login tanpa password -> harus 400// 3. login benar -> harus 200 + access_token// 4. GET list tanpa token -> harus 401// 5. GET list dengan token viewer -> harus 403// 6. GET list dengan token super_admin -> harus 200, TANPA kolom password// 7. akses lewat ?access_token= -> dicatat sebagai kelemahan// 8. create user dengan role terlarang -> harus 400//// Pakai:// BASE_URL=http://localhost:3000 node uji-api.mjs// BASE_URL=http://IP/portal EMAIL=admin@example.com PASSWORD=rahasia node uji-api.mjs// =====================================================================
const BASE = (process.env.BASE_URL ?? 'http://localhost:3000').replace(/\/+$/, '');const EMAIL = process.env.EMAIL ?? 'admin@example.com';const PASSWORD = process.env.PASSWORD ?? '';const TIMEOUT = Number(process.env.TIMEOUT_MS ?? 15000);
const hasil = [];let gagalKritis = false;
async function minta(jalur, opsi = {}) { const kontrol = new AbortController(); const timer = setTimeout(() => kontrol.abort(), TIMEOUT); try { const r = await fetch(BASE + jalur, { ...opsi, signal: kontrol.signal }); const teks = await r.text(); let badan; try { badan = JSON.parse(teks); } catch { badan = { _mentah: teks.slice(0, 200) }; } return { status: r.status, badan }; } catch (e) { return { status: 0, badan: { error: e.message } }; } finally { clearTimeout(timer); }}
function catat(nama, lulus, keterangan) { hasil.push({ nama, lulus, keterangan }); const tanda = lulus === true ? '\u001b[32mLULUS\u001b[0m' : lulus === 'lemah' ? '\u001b[33mLEMAH\u001b[0m' : '\u001b[31mGAGAL\u001b[0m'; console.log(`${tanda} ${nama}`); if (keterangan) console.log(` ${keterangan}`);}
const post = (jalur, body, token) => minta(jalur, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify(body), });
console.log(`Base URL : ${BASE}`);console.log(`Email : ${EMAIL}`);console.log('');
// 1 & 2 ---------------------------------------------------------------const salah = await post('/api/users/login', { email: EMAIL, password: 'jelas-salah-123' });catat('1. Login password salah ditolak', salah.status === 401, `status ${salah.status}`);
const kosong = await post('/api/users/login', { email: EMAIL });catat('2. Login tanpa password ditolak', kosong.status === 400, `status ${kosong.status}`);
// 3 -------------------------------------------------------------------let token = null;let role = null;if (!PASSWORD) { catat('3. Login benar', false, 'Variabel PASSWORD belum diisi, uji login dilewati.'); console.log('\nJalankan ulang dengan: PASSWORD=<password asli> node uji-api.mjs'); console.log('Lewati kalau tidak ada akun; bagian berikutnya butuh token.');} else { const masuk = await post('/api/users/login', { email: EMAIL, password: PASSWORD }); token = masuk.badan?.access_token ?? null; role = masuk.badan?.user?.role ?? null; catat('3. Login benar mengembalikan token', masuk.status === 200 && !!token, `status ${masuk.status}, role ${role ?? '(tidak ada)'}`);}
if (token) { // 4 ----------------------------------------------------------------- const tanpaToken = await minta('/api/users/list'); catat('4. List tanpa token ditolak', tanpaToken.status === 401, `status ${tanpaToken.status}`);
// 5 ----------------------------------------------------------------- // Hanya bermakna kalau akun uji bukan super_admin. if (role !== 'super_admin') { const salahRole = await minta('/api/users/list', { headers: { Authorization: `Bearer ${token}` } }); catat('5. List dengan role di bawah super_admin ditolak', salahRole.status === 403, `status ${salahRole.status}`); } else { catat('5. Uji hierarki role', 'lemah', `Akun uji adalah ${role}, jadi batas 403 tidak bisa diuji dari sini. Jalankan ulang dengan akun viewer.`); }
// 6 ----------------------------------------------------------------- const daftar = await minta('/api/users/list', { headers: { Authorization: `Bearer ${token}` } }); const lulus6 = daftar.status === 200; catat('6a. List dengan token super_admin berhasil', lulus6, `status ${daftar.status}`);
const baris = Array.isArray(daftar.badan?.data) ? daftar.badan.data : []; const adaPassword = baris.some((b) => b && ('password' in b)); if (baris.length > 0) { catat('6b. Respons list tidak memuat kolom password', !adaPassword, adaPassword ? 'Ada field password pada minimal satu baris respons.' : `${baris.length} baris, kolom aman`); }
// 7 ----------------------------------------------------------------- // Kelemahan yang ditemukan di audit-api.mjs: token diterima dari query string. // Query string tercatat di access log, jadi ini memang masalah, bukan sekadar gaya. const viaQuery = await minta(`/api/users/list?access_token=${encodeURIComponent(token)}`); if (viaQuery.status === 200) { catat('7. Token lewat query string ditolak', 'lemah', 'DITERIMA. Token pada URL akan tersimpan di access log Nginx, log Cloud Build, dan history browser.\n' + ' Perbaikan: hapus getTokenParams() di lib/auth/verifyBearerToken.js.'); gagalKritis = true; } else { catat('7. Token lewat query string ditolak', true, `status ${viaQuery.status}`); }
// 8 ----------------------------------------------------------------- const roleTerlarang = await post('/api/users/create', { nama: 'Uji', email: `uji-${Date.now()}@example.com`, password: 'Rahasia123', role: 'super_admin', is_active: true }, token); catat('8. Create dengan role super_admin ditolak', roleTerlarang.status === 400, `status ${roleTerlarang.status}`);
// 9 ----------------------------------------------------------------- // Modul tidak pernah menguji ini: apakah respons create membocorkan hash. const emailUji = `uji-${Date.now()}@example.com`; const dibuat = await post('/api/users/create', { nama: 'Uji Bocor', email: emailUji, password: 'Rahasia123', role: 'editor', is_active: true }, token); if (dibuat.status === 201) { const bocor = dibuat.badan?.data?.password !== undefined; catat('9. Respons create tidak memuat hash password', !bocor, bocor ? 'Objek user dikembalikan apa adanya sehingga hash bcrypt ikut terkirim.' : 'aman'); if (bocor) gagalKritis = true;
// Bersihkan: hapus user uji supaya database tidak menumpuk. const idBaru = dibuat.badan?.data?.user_id; if (idBaru) { await post('/api/users/delete', { user_id: idBaru }, token); console.log(' (user uji dihapus kembali)'); } } else { catat('9. Respons create tidak memuat hash password', 'lemah', `create tidak berhasil (status ${dibuat.status}), jadi pemeriksaan dilewati`); }
// 10 ---------------------------------------------------------------- // update tanpa is_active: apakah kolom is_active ikut dikosongkan? const daftar2 = await minta('/api/users/list', { headers: { Authorization: `Bearer ${token}` } }); const calon = (daftar2.badan?.data ?? []).find((u) => u.role !== 'super_admin'); if (calon) { const sebelum = calon.is_active; const upd = await post('/api/users/update', { user_id: calon.user_id, role: calon.role }, token); const sesudah = upd.badan?.data?.is_active; const berubah = sesudah !== undefined && sesudah !== sebelum; catat('10. Update tanpa is_active tidak mengubah status aktif', !berubah, berubah ? `is_active berubah dari ${sebelum} menjadi ${sesudah}. Field yang tidak dikirim seharusnya tidak diubah.` : `tetap ${sebelum}`); if (berubah) gagalKritis = true; }}
// ------------------------------------------------------------------ ringkasanconst jum = hasil.length;const lulus = hasil.filter((h) => h.lulus === true).length;const lemah = hasil.filter((h) => h.lulus === 'lemah').length;const gagal = hasil.filter((h) => h.lulus === false).length;
console.log('');console.log(`Ringkasan: ${jum} uji — LULUS=${lulus} LEMAH=${lemah} GAGAL=${gagal}`);process.exit(gagal > 0 || gagalKritis ? 1 : 0);