Lewati ke konten

Uji API

Uji alur login dan otorisasi pada aplikasi yang berjalan.

Berkas: alat/hari-3/Praktik 9/tools/uji-api.mjs

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;
}
}
// ------------------------------------------------------------------ ringkasan
const 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);