chore: prepare docker webgis deployment
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$tests = [
|
||||
'test_csrf_enforcement.php',
|
||||
'test_session_cookie_security.php',
|
||||
'test_operator_scope_endpoints.php',
|
||||
'test_operator_scope_strict.php',
|
||||
'test_viewer_public_filters.php',
|
||||
'test_user_management_guards.php',
|
||||
'test_frontend_reliability_guards.php',
|
||||
'test_deployment_docs.php',
|
||||
'test_webroot_test_access_guard.php',
|
||||
'test_navigation_role_guards.php',
|
||||
'test_public_donor_rate_limit.php',
|
||||
'test_csv_hardening.php',
|
||||
'test_safe_error_responses.php',
|
||||
'test_audit_note_ui.php',
|
||||
'test_documentation_sop.php',
|
||||
'test_recalculate_transaction_guard.php',
|
||||
'test_donor_inbox_api.php',
|
||||
'test_donor_inbox_ui.php',
|
||||
];
|
||||
|
||||
$php = PHP_BINARY;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($tests as $test) {
|
||||
$path = __DIR__ . DIRECTORY_SEPARATOR . $test;
|
||||
if (!file_exists($path)) {
|
||||
echo "SKIP: {$test} not found\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "\n=== {$test} ===\n";
|
||||
passthru('"' . $php . '" "' . $path . '"', $code);
|
||||
if ($code !== 0) {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n=== SUMMARY ===\n";
|
||||
echo $failed === 0 ? "ALL PASSED\n" : "{$failed} test file(s) failed\n";
|
||||
exit($failed > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$statusPage = file_get_contents($root . '/pages/status-bantuan.php');
|
||||
$kebutuhanPage = file_get_contents($root . '/pages/kebutuhan.php');
|
||||
$pendudukJs = file_get_contents($root . '/modules/penduduk.js');
|
||||
$kebutuhanJs = file_get_contents($root . '/modules/kebutuhan.js');
|
||||
|
||||
check('status page sends catatan', strpos($statusPage, "fd.append('catatan'") !== false);
|
||||
check('kebutuhan page sends catatan', strpos($kebutuhanPage, "fd.append('catatan'") !== false);
|
||||
check('map penduduk status sends catatan', strpos($pendudukJs, "fd.append('catatan'") !== false);
|
||||
check('map kebutuhan status sends catatan', strpos($kebutuhanJs, "fd.append('catatan'") !== false);
|
||||
check('status UI asks for note', strpos($statusPage, 'prompt(') !== false || strpos($statusPage, 'catatan') !== false);
|
||||
check('status page cancels update when note prompt is cancelled', strpos($statusPage, 'catatan === null') !== false);
|
||||
check('kebutuhan page cancels update when note prompt is cancelled', strpos($kebutuhanPage, 'catatan === null') !== false);
|
||||
check('map penduduk cancels update when note prompt is cancelled', strpos($pendudukJs, 'catatan === null') !== false);
|
||||
check('map kebutuhan cancels update when note prompt is cancelled', strpos($kebutuhanJs, 'catatan === null') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// tests/test_auth_helper.php — Test fungsi di auth/helper.php
|
||||
// Jalankan via browser: http://localhost/webgis/WebgisPovertyMapping/tests/test_auth_helper.php
|
||||
if (PHP_SAPI === 'cli') {
|
||||
$session_path = __DIR__ . '/../tmp/test-sessions';
|
||||
if (!is_dir($session_path)) {
|
||||
mkdir($session_path, 0777, true);
|
||||
}
|
||||
session_save_path($session_path);
|
||||
}
|
||||
require_once '../config.php';
|
||||
|
||||
$pass = 0; $fail = 0;
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) { echo "✅ $label\n"; $pass++; }
|
||||
else { echo "❌ $label\n"; $fail++; }
|
||||
}
|
||||
|
||||
require_once '../auth/helper.php';
|
||||
|
||||
// Test 1: tanpa session → is_logged_in() return false
|
||||
$_SESSION = [];
|
||||
check('is_logged_in() false tanpa session', is_logged_in() === false);
|
||||
|
||||
// Test 2: session ada → is_logged_in() return true
|
||||
$_SESSION['user_id'] = 1;
|
||||
$_SESSION['role'] = 'administrator';
|
||||
$_SESSION['last_activity'] = time();
|
||||
check('is_logged_in() true dengan session', is_logged_in() === true);
|
||||
check('get_role() returns administrator', get_role() === 'administrator');
|
||||
check('has_role(administrator) true', has_role('administrator') === true);
|
||||
check('administrator satisfies operator role', has_role('operator') === true);
|
||||
|
||||
// Test 3: operator hanya bisa akses role operator dan viewer
|
||||
$_SESSION['role'] = 'operator';
|
||||
check('operator: has_role(operator) true', has_role('operator') === true);
|
||||
check('operator: has_role(administrator) false', has_role('administrator') === false);
|
||||
|
||||
// Test 4: get_ibadah_id
|
||||
$_SESSION['ibadah_id'] = 5;
|
||||
check('get_ibadah_id() returns 5', get_ibadah_id() === 5);
|
||||
|
||||
// Test 5: viewer has lowest access
|
||||
$_SESSION['role'] = 'viewer';
|
||||
check('viewer: has_role(viewer) true', has_role('viewer') === true);
|
||||
check('viewer: has_role(operator) false', has_role('operator') === false);
|
||||
|
||||
echo "\n--- $pass passed, $fail failed ---\n";
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$config = file_get_contents($root . '/config.php');
|
||||
check('config defines map minimum latitude', strpos($config, 'MAP_MIN_LAT') !== false);
|
||||
check('config defines map maximum latitude', strpos($config, 'MAP_MAX_LAT') !== false);
|
||||
check('config defines map minimum longitude', strpos($config, 'MAP_MIN_LNG') !== false);
|
||||
check('config defines map maximum longitude', strpos($config, 'MAP_MAX_LNG') !== false);
|
||||
|
||||
$helper = $root . '/includes/validation.php';
|
||||
check('shared validation helper exists', file_exists($helper));
|
||||
|
||||
if (file_exists($helper)) {
|
||||
require_once $root . '/config.php';
|
||||
require_once $helper;
|
||||
|
||||
check('validate_lat_lng function exists', function_exists('validate_lat_lng'));
|
||||
if (function_exists('validate_lat_lng')) {
|
||||
check('valid Pontianak coordinate accepted', validate_lat_lng('-0.0557', '109.3487')['ok'] === true);
|
||||
check('blank coordinate rejected', validate_lat_lng('', '109.3487')['ok'] === false);
|
||||
check('non numeric coordinate rejected', validate_lat_lng('abc', '109.3487')['ok'] === false);
|
||||
check('zero-zero coordinate rejected by study bounds', validate_lat_lng('0', '0')['ok'] === false);
|
||||
check('latitude outside earth range rejected', validate_lat_lng('91', '109.3487')['ok'] === false);
|
||||
check('longitude outside earth range rejected', validate_lat_lng('-0.0557', '181')['ok'] === false);
|
||||
}
|
||||
}
|
||||
|
||||
$mutationFiles = [
|
||||
'api/penduduk/simpan.php',
|
||||
'api/penduduk/update_posisi.php',
|
||||
'api/ibadah/simpan.php',
|
||||
'api/ibadah/update.php',
|
||||
'api/ibadah/update_posisi.php',
|
||||
'api/import/csv.php',
|
||||
];
|
||||
|
||||
foreach ($mutationFiles as $relative) {
|
||||
$src = file_get_contents($root . '/' . $relative);
|
||||
check("{$relative} loads validation helper", strpos($src, 'includes/validation.php') !== false);
|
||||
check("{$relative} calls validate_lat_lng", strpos($src, 'validate_lat_lng(') !== false);
|
||||
}
|
||||
|
||||
$ibadahUpdate = file_get_contents($root . '/api/ibadah/update.php');
|
||||
check(
|
||||
'ibadah update validates submitted coordinates directly',
|
||||
strpos($ibadahUpdate, "validate_lat_lng(\$_POST['lat'] ?? null, \$_POST['lng'] ?? null)") !== false
|
||||
);
|
||||
check(
|
||||
'ibadah update does not substitute existing coordinates for blank input',
|
||||
strpos($ibadahUpdate, 'SELECT lat, lng FROM rumah_ibadah') === false
|
||||
);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$helper = file_get_contents($root . '/auth/helper.php');
|
||||
check('csrf_token function exists', strpos($helper, 'function csrf_token(): string') !== false);
|
||||
check('require_csrf function exists', strpos($helper, 'function require_csrf(): void') !== false);
|
||||
check('require_csrf uses hash_equals', strpos($helper, 'hash_equals') !== false);
|
||||
|
||||
$must_have = [
|
||||
'api/ibadah/simpan.php',
|
||||
'api/ibadah/hapus.php',
|
||||
'api/ibadah/update.php',
|
||||
'api/ibadah/update_posisi.php',
|
||||
'api/ibadah/update_radius.php',
|
||||
'api/ibadah/update_kontak.php',
|
||||
'api/ibadah/recalculate.php',
|
||||
'api/penduduk/simpan.php',
|
||||
'api/penduduk/update_posisi.php',
|
||||
'api/penduduk/update_status.php',
|
||||
'api/penduduk/hapus.php',
|
||||
'api/penduduk/nonaktifkan.php',
|
||||
'api/penduduk/verifikasi.php',
|
||||
'api/kebutuhan/simpan.php',
|
||||
'api/kebutuhan/update_status.php',
|
||||
'api/users/simpan.php',
|
||||
'api/users/update.php',
|
||||
'api/users/reset_password.php',
|
||||
'api/users/toggle_active.php',
|
||||
'api/import/csv.php',
|
||||
'api/auth/change_password.php',
|
||||
'api/papan/donatur.php',
|
||||
];
|
||||
|
||||
foreach ($must_have as $file) {
|
||||
$content = file_get_contents($root . '/' . $file);
|
||||
check("{$file} calls require_csrf", strpos($content, 'require_csrf();') !== false);
|
||||
}
|
||||
|
||||
$index = file_get_contents($root . '/index.php');
|
||||
$pageStart = file_get_contents($root . '/includes/page-start.php');
|
||||
$changePassword = file_get_contents($root . '/auth/change_password.php');
|
||||
|
||||
check('index exposes csrf token on APP_USER', strpos($index, 'csrfToken: <?= json_encode($logged_in ? csrf_token() : null) ?>') !== false);
|
||||
check('index defines appendCsrf helper', strpos($index, 'function appendCsrf(fd)') !== false);
|
||||
check('dashboard pages expose APP_CSRF_TOKEN', strpos($pageStart, 'window.APP_CSRF_TOKEN = <?= json_encode(is_logged_in() ? csrf_token() : null) ?>;') !== false);
|
||||
check('change password appends csrf token', strpos($changePassword, "fd.append('csrf_token',") !== false);
|
||||
check('donor mark-read uses POST with csrf', strpos($index, "fetch('api/papan/donatur.php', { method: 'POST', body: appendCsrf(fd) })") !== false);
|
||||
$donorEndpoint = file_get_contents($root . '/api/papan/donatur.php');
|
||||
check('donor mark-read is POST-only', strpos($donorEndpoint, "\$_SERVER['REQUEST_METHOD'] === 'POST'") !== false);
|
||||
check('donor mark-read requires csrf before update', strpos($donorEndpoint, 'require_csrf()') !== false && strpos($donorEndpoint, 'UPDATE kontak_donatur SET is_read=1') !== false);
|
||||
|
||||
$frontend_files = [
|
||||
'index.php',
|
||||
'modules/ibadah.js',
|
||||
'modules/penduduk.js',
|
||||
'modules/kebutuhan.js',
|
||||
'pages/analisis.php',
|
||||
'pages/ibadah.php',
|
||||
'pages/import.php',
|
||||
'pages/kebutuhan.php',
|
||||
'pages/penduduk.php',
|
||||
'pages/status-bantuan.php',
|
||||
'pages/users.php',
|
||||
];
|
||||
|
||||
foreach ($frontend_files as $file) {
|
||||
$content = file_get_contents($root . '/' . $file);
|
||||
check("{$file} uses appendCsrf", strpos($content, 'appendCsrf(') !== false);
|
||||
}
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$import = file_get_contents($root . '/api/import/csv.php');
|
||||
$export = file_get_contents($root . '/api/export/csv.php');
|
||||
|
||||
check('import checks upload error', strpos($import, 'UPLOAD_ERR_OK') !== false);
|
||||
check('import checks csv extension', strpos($import, 'PATHINFO_EXTENSION') !== false || strpos($import, 'pathinfo(') !== false);
|
||||
check('import checks file size', strpos($import, "csv_file']['size']") !== false || (strpos($import, '$file[\'size\']') !== false && strpos($import, '1024') !== false));
|
||||
check('export defines csv safe cell helper', strpos($export, 'function csv_safe_cell') !== false);
|
||||
check('export neutralizes spreadsheet formulas', strpos($export, "['=', '+', '-', '@']") !== false || strpos($export, 'str_starts_with') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
// tests/test_db.php — Verifikasi schema database PRD v1.0
|
||||
// Jalankan via browser: http://localhost/webgis/WebgisPovertyMapping/tests/test_db.php
|
||||
require_once '../koneksi.php';
|
||||
|
||||
$pass = 0; $fail = 0;
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) { echo "✅ $label\n"; $pass++; }
|
||||
else { echo "❌ $label\n"; $fail++; }
|
||||
}
|
||||
|
||||
// --- rumah_ibadah ---
|
||||
$cols = [];
|
||||
$r = $conn->query("SHOW COLUMNS FROM rumah_ibadah");
|
||||
while ($row = $r->fetch_assoc()) $cols[] = $row['Field'];
|
||||
|
||||
check('rumah_ibadah.updated_at exists', in_array('updated_at', $cols));
|
||||
check('rumah_ibadah.deleted_at exists', in_array('deleted_at', $cols));
|
||||
|
||||
// --- penduduk_miskin ---
|
||||
$cols = [];
|
||||
$r = $conn->query("SHOW COLUMNS FROM penduduk_miskin");
|
||||
while ($row = $r->fetch_assoc()) $cols[] = $row['Field'];
|
||||
|
||||
check('penduduk_miskin.catatan exists', in_array('catatan', $cols));
|
||||
check('penduduk_miskin.status_bantuan exists', in_array('status_bantuan', $cols));
|
||||
check('penduduk_miskin.is_active exists', in_array('is_active', $cols));
|
||||
check('penduduk_miskin.updated_at exists', in_array('updated_at', $cols));
|
||||
check('penduduk_miskin.deleted_at exists', in_array('deleted_at', $cols));
|
||||
|
||||
// --- tabel users ---
|
||||
$t = $conn->query("SHOW TABLES LIKE 'users'");
|
||||
check('table users exists', $t->num_rows > 0);
|
||||
|
||||
// --- tabel riwayat_bantuan ---
|
||||
$t = $conn->query("SHOW TABLES LIKE 'riwayat_bantuan'");
|
||||
check('table riwayat_bantuan exists', $t->num_rows > 0);
|
||||
|
||||
// --- users columns ---
|
||||
if ($conn->query("SHOW TABLES LIKE 'users'")->num_rows > 0) {
|
||||
$cols = [];
|
||||
$r = $conn->query("SHOW COLUMNS FROM users");
|
||||
while ($row = $r->fetch_assoc()) $cols[] = $row['Field'];
|
||||
check('users.username exists', in_array('username', $cols));
|
||||
check('users.role exists', in_array('role', $cols));
|
||||
check('users.must_change_password exists', in_array('must_change_password', $cols));
|
||||
check('users.login_attempts exists', in_array('login_attempts', $cols));
|
||||
check('users.locked_until exists', in_array('locked_until', $cols));
|
||||
check('users.ibadah_id exists', in_array('ibadah_id', $cols));
|
||||
}
|
||||
|
||||
echo "\n--- $pass passed, $fail failed ---\n";
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$docPath = $root . '/docs/deployment-xampp.md';
|
||||
check('deployment XAMPP doc exists', file_exists($docPath));
|
||||
|
||||
if (file_exists($docPath)) {
|
||||
$doc = file_get_contents($docPath);
|
||||
foreach ([
|
||||
'Apache',
|
||||
'PHP',
|
||||
'MySQL',
|
||||
'MariaDB',
|
||||
'DB_PORT',
|
||||
'3307',
|
||||
'setup_database.sql',
|
||||
'admin',
|
||||
'Admin1234',
|
||||
'Leaflet',
|
||||
'Chart.js',
|
||||
'leaflet.heat',
|
||||
'Google Fonts',
|
||||
'OpenStreetMap',
|
||||
'Nominatim',
|
||||
'session timeout',
|
||||
'mirror lokal',
|
||||
'reverse proxy',
|
||||
'Secure',
|
||||
'Lucide',
|
||||
] as $needle) {
|
||||
check("deployment doc mentions {$needle}", strpos($doc, $needle) !== false);
|
||||
}
|
||||
}
|
||||
|
||||
$readme = file_get_contents($root . '/README.md');
|
||||
$nav = file_get_contents($root . '/docs/codebase-navigation.md');
|
||||
check('README links deployment doc', strpos($readme, 'docs/deployment-xampp.md') !== false);
|
||||
check('navigation links deployment doc', strpos($nav, 'deployment-xampp.md') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$sopPath = $root . '/docs/business-process-sop.md';
|
||||
check('business SOP exists', file_exists($sopPath));
|
||||
|
||||
$sop = file_exists($sopPath) ? file_get_contents($sopPath) : '';
|
||||
foreach (['Administrator', 'Operator', 'Publik', 'Donatur', 'Verifikasi', 'Proximity', 'Recalculate', 'Catatan'] as $needle) {
|
||||
check("SOP mentions {$needle}", strpos($sop, $needle) !== false);
|
||||
}
|
||||
|
||||
$readme = file_get_contents($root . '/README.md');
|
||||
check('README links business SOP', strpos($readme, 'docs/business-process-sop.md') !== false);
|
||||
|
||||
$design = file_get_contents($root . '/DESIGN.md');
|
||||
check('DESIGN status is not ambiguous', strpos($design, 'Status dokumen') !== false && strpos($design, 'artifact') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0; $fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) { echo "PASS: {$label}\n"; $pass++; }
|
||||
else { echo "FAIL: {$label}\n"; $fail++; }
|
||||
}
|
||||
|
||||
$api = file_get_contents($root . '/api/papan/donatur.php');
|
||||
$index = file_get_contents($root . '/index.php');
|
||||
|
||||
check('donatur API reads $action from POST', strpos($api, '$action') !== false);
|
||||
check('donatur API handles action=hapus', strpos($api, "'hapus'") !== false && strpos($api, 'DELETE FROM kontak_donatur') !== false);
|
||||
check('donatur API handles action=mark_one', strpos($api, "'mark_one'") !== false && strpos($api, 'UPDATE kontak_donatur SET is_read=1 WHERE id') !== false);
|
||||
check('donatur API uses prepared statement (bind_param)', strpos($api, 'bind_param') !== false);
|
||||
check('donatur API casts id to int', strpos($api, '(int)') !== false);
|
||||
check('donatur API requires CSRF on all POST', strpos($api, 'require_csrf') !== false);
|
||||
check('markDonorRead in index.php sends action=mark_all', strpos($index, "append('action', 'mark_all')") !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0; $fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) { echo "PASS: {$label}\n"; $pass++; }
|
||||
else { echo "FAIL: {$label}\n"; $fail++; }
|
||||
}
|
||||
|
||||
$src = file_get_contents($root . '/pages/kebutuhan.php');
|
||||
|
||||
check('kebutuhan page has tab-nav element', strpos($src, 'class="tab-nav"') !== false);
|
||||
check('kebutuhan page has Pesan Donatur tab button', strpos($src, 'Pesan Donatur') !== false);
|
||||
check('kebutuhan page has donaturBadge element', strpos($src, 'donaturBadge') !== false);
|
||||
check('kebutuhan page has tabKebutuhan panel', strpos($src, 'tabKebutuhan') !== false);
|
||||
check('kebutuhan page has tabDonatur panel', strpos($src, 'tabDonatur') !== false);
|
||||
check('kebutuhan page has switchTab function', strpos($src, 'function switchTab') !== false);
|
||||
check('kebutuhan page has loadDonorInbox function', strpos($src, 'function loadDonorInbox') !== false);
|
||||
check('kebutuhan page has renderDonorInbox function', strpos($src, 'function renderDonorInbox') !== false);
|
||||
check('kebutuhan page has buildKontakUrl function', strpos($src, 'function buildKontakUrl') !== false);
|
||||
check('kebutuhan page has hapusDonatur function', strpos($src, 'function hapusDonatur') !== false);
|
||||
check('kebutuhan page has markAllRead function', strpos($src, 'function markAllRead') !== false);
|
||||
check('kebutuhan page fetches donatur API', strpos($src, 'api/papan/donatur.php') !== false);
|
||||
check('kebutuhan page has admin-only guard', strpos($src, '_IS_ADMIN') !== false || strpos($src, 'is_admin') !== false);
|
||||
check('kebutuhan page has wa.me contact URL logic', strpos($src, 'wa.me') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$file = __DIR__ . '/../index.php';
|
||||
$src = file_get_contents($file);
|
||||
|
||||
if ($src === false) {
|
||||
echo "FAIL: cannot read index.php\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$must_not_contain = [
|
||||
'const TOTAL_MODULES = 4',
|
||||
'loaded === TOTAL_MODULES',
|
||||
"filemtime(__FILE__)",
|
||||
];
|
||||
|
||||
foreach ($must_not_contain as $needle) {
|
||||
if (strpos($src, $needle) !== false) {
|
||||
echo "FAIL: index.php still contains stale loader marker {$needle}\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$must_contain = [
|
||||
'$map_modules',
|
||||
'modules/kebutuhan.js',
|
||||
'window.MAP_MODULES',
|
||||
'window.MAP_MODULES.length',
|
||||
"filemtime(__DIR__ . '/' . \$module)",
|
||||
];
|
||||
|
||||
foreach ($must_contain as $needle) {
|
||||
if (strpos($src, $needle) === false) {
|
||||
echo "FAIL: index.php missing module loader marker {$needle}\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
echo "PASS: frontend module loader uses dynamic module count and per-file cache busting\n";
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$index = file_get_contents($root . '/index.php');
|
||||
$heatmap = file_get_contents($root . '/modules/heatmap.js');
|
||||
$ibadah = file_get_contents($root . '/modules/ibadah.js');
|
||||
$penduduk = file_get_contents($root . '/modules/penduduk.js');
|
||||
$stats = file_get_contents($root . '/modules/stats.js');
|
||||
$kebutuhan = file_get_contents($root . '/modules/kebutuhan.js');
|
||||
$pagePenduduk = file_get_contents($root . '/pages/penduduk.php');
|
||||
$pageStatusBantuan = file_get_contents($root . '/pages/status-bantuan.php');
|
||||
$pageKebutuhan = file_get_contents($root . '/pages/kebutuhan.php');
|
||||
$dashboard = file_get_contents($root . '/dashboard.php');
|
||||
$pageUsers = file_get_contents($root . '/pages/users.php');
|
||||
$pageAnalisis = file_get_contents($root . '/pages/analisis.php');
|
||||
|
||||
check('module loader has onerror handler', strpos($index, 's.onerror = function ()') !== false);
|
||||
check('module loader reports failed script source', strpos($index, 'Gagal memuat modul') !== false);
|
||||
check('heatmap filters numeric coordinates', strpos($heatmap, 'Number.isFinite(lat) && Number.isFinite(lng)') !== false);
|
||||
check('heatmap rejects null coordinate values', strpos($heatmap, 'rawLat !== null') !== false && strpos($heatmap, 'rawLng !== null') !== false);
|
||||
check('heatmap rejects blank coordinate values', strpos($heatmap, "rawLat !== ''") !== false && strpos($heatmap, "rawLng !== ''") !== false);
|
||||
check('recalc UI uses tb-icon selector', strpos($ibadah, "querySelector('.tb-icon')") !== false);
|
||||
check('recalc UI no longer uses ft-icon selector', strpos($ibadah, "querySelector('.ft-icon')") === false);
|
||||
check('global data changed dispatcher is defined', strpos($index, 'window.dispatchDataChanged') !== false && strpos($index, 'webgis:data-changed') !== false);
|
||||
check('global refreshAllData is defined', strpos($index, 'window.refreshAllData') !== false);
|
||||
check('ibadah exposes reload hook', strpos($ibadah, 'window._ibadahReload') !== false);
|
||||
check('stats update guards missing modalStats', strpos($stats, 'const modal = document.getElementById(\'modalStats\')') !== false && strpos($stats, 'if (!modal) return;') !== false);
|
||||
check('stats show/hide guards missing modalStats', strpos($stats, 'if (!modal) return;') !== false && strpos($stats, 'modal.classList.add(\'show\')') !== false && strpos($stats, 'modal.classList.remove(\'show\')') !== false);
|
||||
check('penduduk exposes all dataset', strpos($penduduk, 'window._pendudukAll') !== false);
|
||||
check('penduduk exposes visible dataset', strpos($penduduk, 'window._pendudukVisible') !== false);
|
||||
check('heatmap uses all penduduk dataset by default', strpos($heatmap, 'window._pendudukAll') !== false);
|
||||
check('heatmap blocks public viewer use', strpos($heatmap, '!window.APP_USER?.isOp') !== false && strpos($heatmap, '!window.APP_USER?.isAdmin') !== false);
|
||||
check('penduduk popup passes verification status to kebutuhan section', strpos($penduduk, '_kebutuhanBuildSection(data.id, data.kebutuhan_open, sv)') !== false);
|
||||
check('penduduk popup shows waiting message for unverified workflow actions', strpos($penduduk, 'Menunggu verifikasi admin') !== false);
|
||||
check('kebutuhan popup section accepts verification status', strpos($kebutuhan, 'function (pendudukId, kebutuhanOpen, statusVerifikasi') !== false);
|
||||
check('kebutuhan popup disables mutation controls for unverified warga', strpos($kebutuhan, "statusVerifikasi === 'Terverifikasi'") !== false && strpos($kebutuhan, 'Menunggu verifikasi admin') !== false);
|
||||
check('penduduk page disables status bantuan for unverified warga', strpos($pagePenduduk, 'Menunggu verifikasi admin') !== false && strpos($pagePenduduk, 'disabled') !== false);
|
||||
check('status bantuan page filters only verified warga', strpos($pageStatusBantuan, "status_verifikasi === 'Terverifikasi'") !== false);
|
||||
check('kebutuhan page filters only verified warga', strpos($pageKebutuhan, "status_verifikasi === 'Terverifikasi'") !== false);
|
||||
check('dashboard notification panel uses unique id', strpos($dashboard, 'id="dash-notif-list"') !== false && strpos($dashboard, "getElementById('dash-notif-list')") !== false);
|
||||
check('dashboard boot waits for external scripts', strpos($dashboard, "window.addEventListener('load'") !== false && strpos($dashboard, 'loadStats();') !== false && strpos($dashboard, 'loadTren();') !== false);
|
||||
check('dashboard notifications use canonical endpoint', strpos($dashboard, "fetch('api/notif/ambil.php") !== false && strpos($dashboard, 'loadDashboardNotifs();') !== false);
|
||||
check('dashboard no longer derives notification badge from donor unread only', strpos($dashboard, 'const unread = parseInt(d.donatur_unread') === false);
|
||||
check('dashboard notification rows are clickable links', strpos($dashboard, '<a href="${esc(it.page || \'#\')}" class="notif-item">') !== false);
|
||||
check('penduduk table verification asks confirmation', strpos($pagePenduduk, 'Konfirmasi Verifikasi') !== false && strpos($pagePenduduk, 'await showConfirm') !== false && strpos($pagePenduduk, 'if (!ok) return;') !== false);
|
||||
check('penduduk map verification asks confirmation', strpos($penduduk, 'Konfirmasi Verifikasi') !== false && strpos($penduduk, 'showDeleteConfirm') !== false && strpos($penduduk, 'if (!confirmed) return;') !== false);
|
||||
check('user active toggle asks confirmation', strpos($pageUsers, 'await showConfirm') !== false && strpos($pageUsers, 'Nonaktifkan Akun') !== false && strpos($pageUsers, 'Aktifkan Akun') !== false && strpos($pageUsers, 'if (!ok) return;') !== false);
|
||||
check('analysis recalculate asks confirmation', strpos($pageAnalisis, 'Hitung Ulang Proximity') !== false && strpos($pageAnalisis, 'await showConfirm') !== false && strpos($pageAnalisis, 'if (!ok) return;') !== false);
|
||||
check('map recalculate asks confirmation', strpos($ibadah, 'Konfirmasi Hitung Ulang Proximity') !== false && strpos($ibadah, "btnLabel: 'Ya, Hitung Ulang'") !== false && strpos($ibadah, 'showDeleteConfirm') !== false && strpos($ibadah, 'if (!confirmed) return;') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
// tests/test_ibadah.php — Smoke test API ibadah
|
||||
header('Content-Type: text/plain');
|
||||
if (PHP_SAPI === 'cli') {
|
||||
$session_path = __DIR__ . '/../tmp/test-sessions';
|
||||
if (!is_dir($session_path)) {
|
||||
mkdir($session_path, 0777, true);
|
||||
}
|
||||
session_save_path($session_path);
|
||||
}
|
||||
require_once '../config.php';
|
||||
require_once '../auth/helper.php';
|
||||
require_once '../koneksi.php';
|
||||
|
||||
$pass = 0; $fail = 0;
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) { echo "PASS $label\n"; $pass++; }
|
||||
else { echo "FAIL $label\n"; $fail++; }
|
||||
}
|
||||
|
||||
// Test 1: schema punya deleted_at
|
||||
$cols = [];
|
||||
$r = $conn->query("SHOW COLUMNS FROM rumah_ibadah");
|
||||
while ($row = $r->fetch_assoc()) $cols[] = $row['Field'];
|
||||
check('rumah_ibadah.deleted_at ada', in_array('deleted_at', $cols));
|
||||
|
||||
// Test 2: query dengan filter deleted_at IS NULL bekerja
|
||||
$r2 = $conn->query("SELECT COUNT(*) AS n FROM rumah_ibadah WHERE deleted_at IS NULL");
|
||||
$active = (int)$r2->fetch_assoc()['n'];
|
||||
$r3 = $conn->query("SELECT COUNT(*) AS n FROM rumah_ibadah");
|
||||
$all = (int)$r3->fetch_assoc()['n'];
|
||||
check('query deleted_at IS NULL tidak error', $active <= $all);
|
||||
|
||||
// Test 3: validasi radius 100-5000
|
||||
$clamp_low = max(100, min(5000, 50));
|
||||
$clamp_high = max(100, min(5000, 6000));
|
||||
check('radius < 100 di-clamp ke 100', $clamp_low === 100);
|
||||
check('radius > 5000 di-clamp ke 5000', $clamp_high === 5000);
|
||||
|
||||
$hapus = file_get_contents(__DIR__ . '/../api/ibadah/hapus.php');
|
||||
$kontak = file_get_contents(__DIR__ . '/../api/ibadah/update_kontak.php');
|
||||
$update = file_get_contents(__DIR__ . '/../api/ibadah/update.php');
|
||||
$posisi = file_get_contents(__DIR__ . '/../api/ibadah/update_posisi.php');
|
||||
$radius = file_get_contents(__DIR__ . '/../api/ibadah/update_radius.php');
|
||||
check('hapus filters deleted rows', strpos($hapus, 'AND deleted_at IS NULL') !== false);
|
||||
check('hapus checks affected rows before recalc', strpos($hapus, 'affected_rows') !== false);
|
||||
check('hapus returns deleted/not-found message', strpos($hapus, 'Data tidak ditemukan atau sudah dihapus') !== false);
|
||||
check('hapus checks active ibadah before operator warning', strpos($hapus, 'SELECT id FROM rumah_ibadah') !== false && strpos($hapus, 'SELECT id FROM rumah_ibadah') < strpos($hapus, 'SELECT nama_lengkap FROM users'));
|
||||
check('update_kontak filters deleted rows', strpos($kontak, 'AND deleted_at IS NULL') !== false);
|
||||
check('update_kontak checks active row before update', strpos($kontak, 'SELECT id FROM rumah_ibadah') !== false && strpos($kontak, 'SELECT id FROM rumah_ibadah') < strpos($kontak, 'UPDATE rumah_ibadah'));
|
||||
check('update_kontak returns deleted/not-found message', strpos($kontak, 'Data tidak ditemukan atau sudah dihapus') !== false);
|
||||
foreach ([
|
||||
'update.php' => $update,
|
||||
'update_posisi.php' => $posisi,
|
||||
'update_radius.php' => $radius,
|
||||
] as $file => $src) {
|
||||
check("{$file} checks active row before update", strpos($src, 'SELECT id FROM rumah_ibadah') !== false && strpos($src, 'SELECT id FROM rumah_ibadah') < strpos($src, 'UPDATE rumah_ibadah'));
|
||||
check("{$file} does not treat no-op affected_rows as not found", strpos($src, 'affected_rows === 0') === false);
|
||||
}
|
||||
|
||||
echo "\n--- $pass passed, $fail failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
require_once '../config.php';
|
||||
require_once '../koneksi.php';
|
||||
|
||||
$pass = 0; $fail = 0;
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) { echo "✅ $label\n"; $pass++; }
|
||||
else { echo "❌ $label\n"; $fail++; }
|
||||
}
|
||||
|
||||
// ── Tabel kebutuhan ────────────────────────────────────────────────────────
|
||||
$cols = [];
|
||||
$r = $conn->query("SHOW COLUMNS FROM kebutuhan");
|
||||
while ($row = $r->fetch_assoc()) $cols[] = $row['Field'];
|
||||
check('kebutuhan.id exists', in_array('id', $cols));
|
||||
check('kebutuhan.penduduk_id exists', in_array('penduduk_id', $cols));
|
||||
check('kebutuhan.kategori exists', in_array('kategori', $cols));
|
||||
check('kebutuhan.deskripsi exists', in_array('deskripsi', $cols));
|
||||
check('kebutuhan.status exists', in_array('status', $cols));
|
||||
check('kebutuhan.created_by exists', in_array('created_by', $cols));
|
||||
|
||||
// ── Tabel riwayat_kebutuhan ────────────────────────────────────────────────
|
||||
$cols2 = [];
|
||||
$r2 = $conn->query("SHOW COLUMNS FROM riwayat_kebutuhan");
|
||||
while ($row = $r2->fetch_assoc()) $cols2[] = $row['Field'];
|
||||
check('riwayat_kebutuhan.kebutuhan_id exists', in_array('kebutuhan_id', $cols2));
|
||||
check('riwayat_kebutuhan.operator_id exists', in_array('operator_id', $cols2));
|
||||
check('riwayat_kebutuhan.status_baru exists', in_array('status_baru', $cols2));
|
||||
|
||||
// ── Tabel kontak_donatur ───────────────────────────────────────────────────
|
||||
$cols3 = [];
|
||||
$r3 = $conn->query("SHOW COLUMNS FROM kontak_donatur");
|
||||
while ($row = $r3->fetch_assoc()) $cols3[] = $row['Field'];
|
||||
check('kontak_donatur.nama exists', in_array('nama', $cols3));
|
||||
check('kontak_donatur.kontak exists', in_array('kontak', $cols3));
|
||||
check('kontak_donatur.is_read exists', in_array('is_read', $cols3));
|
||||
|
||||
// ── Index kebutuhan ────────────────────────────────────────────────────────
|
||||
$idx = $conn->query("SHOW INDEX FROM kebutuhan");
|
||||
$idx_names = [];
|
||||
while ($row = $idx->fetch_assoc()) $idx_names[] = $row['Key_name'];
|
||||
check('kebutuhan penduduk index exists', in_array('idx_k_penduduk', $idx_names) || in_array('idx_penduduk', $idx_names));
|
||||
check('kebutuhan status index exists', in_array('idx_k_status', $idx_names) || in_array('idx_status', $idx_names));
|
||||
|
||||
// ── Default status = Belum Terpenuhi ─────────────────────────────────────
|
||||
$default_status = $conn->query(
|
||||
"SELECT COLUMN_DEFAULT FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='kebutuhan' AND COLUMN_NAME='status'"
|
||||
)->fetch_assoc()['COLUMN_DEFAULT'];
|
||||
$default_status = trim((string)$default_status, "'");
|
||||
check("kebutuhan.status default = 'Belum Terpenuhi'", $default_status === 'Belum Terpenuhi');
|
||||
|
||||
// ── kontak_donatur.is_read default = 0 ───────────────────────────────────
|
||||
$default_read = $conn->query(
|
||||
"SELECT COLUMN_DEFAULT FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='kontak_donatur' AND COLUMN_NAME='is_read'"
|
||||
)->fetch_assoc()['COLUMN_DEFAULT'];
|
||||
check("kontak_donatur.is_read default = 0", $default_read === '0');
|
||||
|
||||
// ── ambil.php LEFT JOIN returns kebutuhan_open column ─────────────────────
|
||||
$test_sql = "
|
||||
SELECT pm.id, COALESCE(kstat.kebutuhan_open, 0) AS kebutuhan_open
|
||||
FROM penduduk_miskin pm
|
||||
LEFT JOIN (
|
||||
SELECT penduduk_id, SUM(CASE WHEN status='Belum Terpenuhi' THEN 1 ELSE 0 END) AS kebutuhan_open
|
||||
FROM kebutuhan GROUP BY penduduk_id
|
||||
) kstat ON kstat.penduduk_id = pm.id
|
||||
WHERE pm.is_active = 1 AND pm.deleted_at IS NULL
|
||||
LIMIT 1
|
||||
";
|
||||
$q = $conn->query($test_sql);
|
||||
check('kebutuhan LEFT JOIN query executes without error', $q !== false);
|
||||
|
||||
echo "\n--- $pass passed, $fail failed ---\n";
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$map = file_get_contents($root . '/pages/map.php');
|
||||
$pageStart = file_get_contents($root . '/includes/page-start.php');
|
||||
$index = file_get_contents($root . '/index.php');
|
||||
$dashboard = file_get_contents($root . '/dashboard.php');
|
||||
$laporan = file_get_contents($root . '/pages/laporan.php');
|
||||
|
||||
check(
|
||||
'pages/map.php requires login before admin layout',
|
||||
strpos($map, "if (!is_logged_in()") !== false
|
||||
&& strpos($map, "header('Location: ../auth/login.php')") !== false
|
||||
);
|
||||
check('sidebar has administrator branch', strpos($pageStart, "\$_nav_role === 'administrator'") !== false);
|
||||
check('sidebar has operator branch', strpos($pageStart, "\$_nav_role === 'operator'") !== false);
|
||||
check('admin-only import is inside administrator branch', strpos($pageStart, "pages/import.php") !== false && strpos($pageStart, "\$_nav_role === 'administrator'") !== false);
|
||||
check('operator map dropdown avoids administrator dashboard', strpos($index, "has_role('administrator')") !== false && strpos($index, 'pages/status-bantuan.php') !== false);
|
||||
check('dashboard has no href dead link', strpos($dashboard, 'href="#"') === false);
|
||||
check('laporan export buttons are disabled before selection', strpos($laporan, 'aria-disabled="true"') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$removed_paths = [
|
||||
'modules/point.js',
|
||||
'api/point',
|
||||
'ambil_data.php',
|
||||
'simpan.php',
|
||||
'hapus.php',
|
||||
'update_posisi.php',
|
||||
];
|
||||
|
||||
foreach ($removed_paths as $path) {
|
||||
check("{$path} removed", !file_exists($root . '/' . $path));
|
||||
}
|
||||
|
||||
$scan_files = [
|
||||
'index.php',
|
||||
'modules/penduduk.js',
|
||||
'includes/page-start.php',
|
||||
'setup_database.sql',
|
||||
'README.md',
|
||||
'docs/codebase-navigation.md',
|
||||
];
|
||||
|
||||
$forbidden = [
|
||||
'lokasi_usaha',
|
||||
'modules/point.js',
|
||||
'api/point',
|
||||
'initPoint',
|
||||
'_point',
|
||||
'ambil_data.php',
|
||||
'nama_tempat',
|
||||
'searchPoi',
|
||||
'Data POI',
|
||||
'Pengaturan Sistem',
|
||||
'Aktivitas & Audit Log',
|
||||
'POI/business',
|
||||
];
|
||||
|
||||
foreach ($scan_files as $file) {
|
||||
$content = file_get_contents($root . '/' . $file);
|
||||
$content = str_replace('test_no_legacy_poi.php', 'test_no_legacy_regression.php', $content);
|
||||
foreach ($forbidden as $needle) {
|
||||
check("{$file} has no {$needle}", strpos($content, $needle) === false);
|
||||
}
|
||||
}
|
||||
|
||||
$nav = file_get_contents($root . '/includes/page-start.php');
|
||||
check('sidebar has no visible placeholder href menu item', strpos($nav, "_sb_item('#'") === false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$index = file_get_contents($root . '/index.php');
|
||||
$ibadah = file_get_contents($root . '/modules/ibadah.js');
|
||||
|
||||
check('APP_USER exposes operator ibadah id', strpos($index, 'ibadahId: <?= json_encode($is_op ? get_ibadah_id() : null) ?>') !== false);
|
||||
check('ibadah module can detect current operator marker', strpos($ibadah, 'function isOwnOperatorIbadah(id)') !== false);
|
||||
check('operator highlight uses gold accent', strpos($ibadah, 'OPERATOR_HIGHLIGHT') !== false && strpos($ibadah, '#f59e0b') !== false);
|
||||
check('operator marker icon receives own marker flag', strpos($ibadah, 'createIbadahIcon(jenis, ownOperatorIbadah)') !== false);
|
||||
check('operator circle uses highlighted style', strpos($ibadah, 'ownOperatorIbadah ? OPERATOR_HIGHLIGHT.stroke : clr.stroke') !== false);
|
||||
check('operator marker gets higher z-index', strpos($ibadah, 'ownOperatorIbadah ? 1000 : 100') !== false);
|
||||
check('operator popup shows own ibadah badge', strpos($ibadah, 'Rumah ibadah Anda') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$files = [
|
||||
'api/penduduk/update_status.php',
|
||||
'api/penduduk/riwayat.php',
|
||||
'api/kebutuhan/ambil.php',
|
||||
'api/kebutuhan/simpan.php',
|
||||
'api/kebutuhan/update_status.php',
|
||||
];
|
||||
|
||||
foreach ($files as $relative) {
|
||||
$path = __DIR__ . '/../' . $relative;
|
||||
$src = file_get_contents($path);
|
||||
|
||||
if ($src === false) {
|
||||
echo "FAIL: cannot read {$relative}\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$checks = [
|
||||
'operator auth' => "require_auth('operator')",
|
||||
'admin bypass' => "has_role('administrator')",
|
||||
'operator ibadah lookup' => 'get_ibadah_id()',
|
||||
'forbidden status code' => 'http_response_code(403)',
|
||||
];
|
||||
|
||||
foreach ($checks as $label => $needle) {
|
||||
if (strpos($src, $needle) === false) {
|
||||
echo "FAIL: {$relative} missing {$label}\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ([
|
||||
'api/kebutuhan/simpan.php',
|
||||
'api/kebutuhan/update_status.php',
|
||||
'api/penduduk/update_status.php',
|
||||
] as $relative) {
|
||||
$src = file_get_contents(__DIR__ . '/../' . $relative);
|
||||
if (strpos($src, 'status_verifikasi') === false) {
|
||||
echo "FAIL: {$relative} missing verification status guard\n";
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($src, 'Terverifikasi') === false) {
|
||||
echo "FAIL: {$relative} missing Terverifikasi comparison\n";
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($src, 'Data warga belum terverifikasi') === false) {
|
||||
echo "FAIL: {$relative} missing unverified warga error message\n";
|
||||
exit(1);
|
||||
}
|
||||
$guardPos = strpos($src, "!== 'Terverifikasi'");
|
||||
$writeNeedle = str_contains($relative, 'simpan.php')
|
||||
? 'INSERT INTO kebutuhan'
|
||||
: (str_contains($relative, 'kebutuhan') ? 'UPDATE kebutuhan' : 'UPDATE penduduk_miskin');
|
||||
$writePos = strpos($src, $writeNeedle);
|
||||
if ($guardPos === false || $writePos === false || $guardPos > $writePos) {
|
||||
echo "FAIL: {$relative} verification guard must run before {$writeNeedle}\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
echo "PASS: operator scoped endpoints include auth, scope checks, and 403 responses\n";
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$simpan = file_get_contents($root . '/api/penduduk/simpan.php');
|
||||
$move = file_get_contents($root . '/api/penduduk/update_posisi.php');
|
||||
$stats = file_get_contents($root . '/api/stats/ambil.php');
|
||||
|
||||
check(
|
||||
'simpan rejects operator without linked ibadah',
|
||||
strpos($simpan, 'Operator belum dikaitkan dengan rumah ibadah') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'simpan rejects calculated ibadah outside operator scope',
|
||||
strpos($simpan, 'Lokasi warga berada di luar wilayah rumah ibadah operator') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'simpan checks proximity result against operator ibadah',
|
||||
strpos($simpan, '(int)$p[\'ibadah_id\'] !== $op_ibadah_id') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'update_posisi checks new proximity result against operator ibadah',
|
||||
strpos($move, '(int)$p[\'ibadah_id\'] !== $linked_ibadah_id') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'update_posisi rejects move outside operator scope',
|
||||
strpos($move, 'Posisi baru berada di luar wilayah rumah ibadah operator') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'stats defines operator scope sql',
|
||||
strpos($stats, '$operator_scope_pm_sql') !== false && strpos($stats, 'get_ibadah_id()') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'stats applies operator scope to penduduk aliases',
|
||||
strpos($stats, 'pm.ibadah_id') !== false && strpos($stats, 'AND 1 = 0') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'stats scopes total ibadah for operators',
|
||||
strpos($stats, 'id = ' . '$operator_ibadah_id') !== false || strpos($stats, 'id = {$operator_ibadah_id}') !== false
|
||||
);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
require_once '../config.php';
|
||||
require_once '../koneksi.php';
|
||||
|
||||
$pass = 0; $fail = 0;
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) { echo "✅ $label\n"; $pass++; }
|
||||
else { echo "❌ $label\n"; $fail++; }
|
||||
}
|
||||
|
||||
// Test 1: kolom baru ada
|
||||
$cols = [];
|
||||
$r = $conn->query("SHOW COLUMNS FROM penduduk_miskin");
|
||||
while ($row = $r->fetch_assoc()) $cols[] = $row['Field'];
|
||||
check('penduduk_miskin.catatan exists', in_array('catatan', $cols));
|
||||
check('penduduk_miskin.status_bantuan exists', in_array('status_bantuan', $cols));
|
||||
check('penduduk_miskin.is_active exists', in_array('is_active', $cols));
|
||||
check('penduduk_miskin.deleted_at exists', in_array('deleted_at', $cols));
|
||||
|
||||
// Test 2: NIK UNIQUE constraint ada
|
||||
$r = $conn->query("SHOW INDEX FROM penduduk_miskin WHERE Key_name = 'uq_nik'");
|
||||
check('NIK unique index ada', $r->num_rows > 0);
|
||||
|
||||
// Test 3: ambil query hanya mengembalikan is_active=1 dan deleted_at IS NULL
|
||||
$conn->query(
|
||||
"INSERT IGNORE INTO penduduk_miskin (nama_kk, jumlah_jiwa, kategori, lat, lng, is_active)
|
||||
VALUES ('__test_inactive__', 1, 'Miskin', 0, 0, 0)"
|
||||
);
|
||||
$test_id = $conn->insert_id;
|
||||
$n_active = (int)$conn->query(
|
||||
"SELECT COUNT(*) AS n FROM penduduk_miskin WHERE is_active=1 AND deleted_at IS NULL"
|
||||
)->fetch_assoc()['n'];
|
||||
$n_all = (int)$conn->query(
|
||||
"SELECT COUNT(*) AS n FROM penduduk_miskin"
|
||||
)->fetch_assoc()['n'];
|
||||
check('inactive warga tidak tampil di query aktif', $n_all > $n_active);
|
||||
|
||||
// Cleanup
|
||||
if ($test_id) $conn->query("DELETE FROM penduduk_miskin WHERE id = $test_id");
|
||||
|
||||
echo "\n--- $pass passed, $fail failed ---\n";
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$simpan = file_get_contents($root . '/api/penduduk/simpan.php');
|
||||
$import = file_get_contents($root . '/api/import/csv.php');
|
||||
|
||||
check(
|
||||
'penduduk simpan bind types keep jarak_m double and is_blank_spot integer',
|
||||
strpos($simpan, "bind_param(\n 'ssisssddidisis'") !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'import csv bind types include verification fields after proximity fields',
|
||||
strpos($import, "bind_param(\n 'ssisssddidisis'") !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'import csv inserts verification metadata',
|
||||
strpos($import, 'status_verifikasi, verified_by, verified_at') !== false
|
||||
&& strpos($import, "\$status_verif = 'Terverifikasi'") !== false
|
||||
&& strpos($import, '$verified_by = get_user_id()') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'penduduk simpan duplicate NIK checks archived records too',
|
||||
preg_match('/SELECT\s+id,\s+nama_kk\s+FROM\s+penduduk_miskin\s+WHERE\s+nik\s+=\s+\?\s+LIMIT\s+1/s', $simpan) === 1
|
||||
);
|
||||
|
||||
check(
|
||||
'penduduk simpan duplicate NIK message points admin to archive',
|
||||
strpos($simpan, 'pernah terdaftar') !== false && strpos($simpan, 'arsip') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'import csv duplicate NIK checks archived records too',
|
||||
preg_match('/SELECT\s+id\s+FROM\s+penduduk_miskin\s+WHERE\s+nik=\?\s+LIMIT\s+1/s', $import) === 1
|
||||
);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
require_once '../config.php';
|
||||
require_once '../koneksi.php';
|
||||
|
||||
$pass = 0; $fail = 0;
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) { echo "✅ $label\n"; $pass++; }
|
||||
else { echo "❌ $label\n"; $fail++; }
|
||||
}
|
||||
|
||||
// Test 1: haversine helper ada
|
||||
check('haversine_distance() ada', function_exists('haversine_distance'));
|
||||
|
||||
// Test 2: jarak titik ke dirinya sendiri = 0
|
||||
$d = haversine_distance(-0.0557, 109.3487, -0.0557, 109.3487);
|
||||
check('haversine jarak sama = 0', $d < 0.001);
|
||||
|
||||
// Test 3: ~1 derajat latitude ≈ 111 km
|
||||
$d2 = haversine_distance(0, 0, 1, 0);
|
||||
check('haversine ~111km per derajat', $d2 > 110000 && $d2 < 112000);
|
||||
|
||||
// Test 4: tie-breaking — dua ibadah di titik identik → id lebih kecil menang
|
||||
$conn->query("INSERT INTO rumah_ibadah (nama, jenis, lat, lng, radius_m)
|
||||
VALUES ('__test_A__','Masjid', -0.05, 109.34, 5000)");
|
||||
$id_a = $conn->insert_id;
|
||||
$conn->query("INSERT INTO rumah_ibadah (nama, jenis, lat, lng, radius_m)
|
||||
VALUES ('__test_B__','Masjid', -0.05, 109.34, 5000)");
|
||||
$id_b = $conn->insert_id;
|
||||
|
||||
$p = calc_proximity($conn, -0.05, 109.34);
|
||||
check('tie-breaking: ibadah id terkecil menang', (int)$p['ibadah_id'] === (int)$id_a);
|
||||
check('tie-breaking: is_blank_spot=0', $p['is_blank_spot'] === 0);
|
||||
|
||||
// Cleanup
|
||||
$conn->query("DELETE FROM rumah_ibadah WHERE nama IN ('__test_A__','__test_B__')");
|
||||
|
||||
echo "\n--- $pass passed, $fail failed ---\n";
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$endpoint = file_get_contents($root . '/api/papan/kontak_donatur.php');
|
||||
|
||||
check('donor endpoint starts session for rate limit', strpos($endpoint, 'session_start') !== false || strpos($endpoint, 'auth/helper.php') !== false);
|
||||
check('donor endpoint records last submission time', strpos($endpoint, 'donor_last_submit') !== false);
|
||||
check('donor endpoint returns 429 on too many submissions', strpos($endpoint, 'http_response_code(429)') !== false);
|
||||
check('donor endpoint has simple time window', strpos($endpoint, 'time()') !== false && strpos($endpoint, '60') !== false);
|
||||
check('donor endpoint uses client IP for server-side rate limit', strpos($endpoint, 'REMOTE_ADDR') !== false && strpos($endpoint, 'hash(') !== false);
|
||||
check('donor endpoint stores server-side rate limit file', strpos($endpoint, 'file_put_contents') !== false && strpos($endpoint, 'donor-rate-limit') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$files = [
|
||||
'api/stats/ambil.php' => [
|
||||
'pm.status_verifikasi',
|
||||
'$public_verif_sql',
|
||||
'$public_verif_pm_sql',
|
||||
],
|
||||
'api/papan/ambil.php' => [
|
||||
"pm.status_verifikasi = 'Terverifikasi'",
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($files as $relative => $needles) {
|
||||
$path = __DIR__ . '/../' . $relative;
|
||||
$src = file_get_contents($path);
|
||||
|
||||
if ($src === false) {
|
||||
echo "FAIL: cannot read {$relative}\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
foreach ($needles as $needle) {
|
||||
if (strpos($src, $needle) === false) {
|
||||
echo "FAIL: {$relative} missing public verification filter marker {$needle}\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$board = file_get_contents(__DIR__ . '/../papan-kebutuhan.php');
|
||||
if (strpos($board, 'data-kategori') === false) {
|
||||
echo "FAIL: papan-kebutuhan.php missing data-kategori handler\n";
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($board, 'addEventListener') === false) {
|
||||
echo "FAIL: papan-kebutuhan.php missing delegated event listener\n";
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($board, "openForm('\${esc(row.kategori)}')") !== false) {
|
||||
echo "FAIL: papan-kebutuhan.php still uses inline JS string category handler\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "PASS: public stats and board endpoints filter unverified penduduk\n";
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ([
|
||||
'api/ibadah/update.php',
|
||||
'api/ibadah/update_posisi.php',
|
||||
'api/ibadah/update_radius.php',
|
||||
'api/ibadah/hapus.php',
|
||||
'api/ibadah/recalculate.php',
|
||||
] as $file) {
|
||||
$content = file_get_contents($root . '/' . $file);
|
||||
check("{$file} starts transaction", strpos($content, 'begin_transaction') !== false);
|
||||
check("{$file} commits transaction", strpos($content, 'commit()') !== false);
|
||||
check("{$file} rolls back transaction", strpos($content, 'rollback()') !== false);
|
||||
}
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$koneksi = file_get_contents($root . '/koneksi.php');
|
||||
$ambil = file_get_contents($root . '/api/penduduk/ambil.php');
|
||||
$simpan = file_get_contents($root . '/api/penduduk/simpan.php');
|
||||
|
||||
check('db connection logs internal error', strpos($koneksi, 'error_log') !== false);
|
||||
check('db connection does not append connect_error to response', strpos($koneksi, "'message' => 'Koneksi database gagal: ' . \$conn->connect_error") === false);
|
||||
check('penduduk ambil logs query error', strpos($ambil, 'error_log') !== false);
|
||||
check('penduduk ambil does not expose conn error in JSON', strpos($ambil, "'message' => \$conn->error") === false);
|
||||
check('penduduk simpan logs stmt error', strpos($simpan, 'error_log') !== false);
|
||||
check('penduduk simpan does not expose stmt error in JSON', strpos($simpan, "'message' => 'Gagal menyimpan: ' . \$stmt->error") === false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$helper = file_get_contents($root . '/auth/helper.php');
|
||||
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
check('cookie httponly enabled', strpos($helper, "'httponly' => true") !== false);
|
||||
check('cookie samesite strict enabled', strpos($helper, "'samesite' => 'Strict'") !== false);
|
||||
check('cookie secure detects HTTPS', strpos($helper, "'secure' => (!empty(\$_SERVER['HTTPS']) && \$_SERVER['HTTPS'] !== 'off')") !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$file = __DIR__ . '/../api/penduduk/update_posisi.php';
|
||||
$src = file_get_contents($file);
|
||||
|
||||
if ($src === false) {
|
||||
echo "FAIL: cannot read api/penduduk/update_posisi.php\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$checks = [
|
||||
"non-admin branch" => "has_role('administrator')",
|
||||
"operator ibadah lookup" => "get_ibadah_id()",
|
||||
"scope query" => "SELECT id FROM penduduk_miskin",
|
||||
"ibadah scope condition" => "ibadah_id = ?",
|
||||
"forbidden status" => "http_response_code(403)",
|
||||
];
|
||||
|
||||
foreach ($checks as $label => $needle) {
|
||||
if (strpos($src, $needle) === false) {
|
||||
echo "FAIL: missing {$label} scope guard in update_posisi.php\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
echo "PASS: update_posisi operator scope guard is present\n";
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$update = file_get_contents($root . '/api/users/update.php');
|
||||
$simpan = file_get_contents($root . '/api/users/simpan.php');
|
||||
$toggle = file_get_contents($root . '/api/users/toggle_active.php');
|
||||
|
||||
check('users update rejects self-demotion', strpos($update, 'Tidak dapat menurunkan role akun sendiri') !== false);
|
||||
check('users update prevents removing last active administrator', strpos($update, 'administrator aktif terakhir') !== false);
|
||||
check('users update validates operator ibadah is active', strpos($update, 'rumah_ibadah') !== false && strpos($update, 'deleted_at IS NULL') !== false);
|
||||
check('users update clears ibadah for non-operator', strpos($update, '$ibadah_id = null') !== false);
|
||||
|
||||
check('users simpan validates operator ibadah is active', strpos($simpan, 'rumah_ibadah') !== false && strpos($simpan, 'deleted_at IS NULL') !== false);
|
||||
check('users toggle prevents deactivating last active administrator', strpos($toggle, 'administrator aktif terakhir') !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$file = __DIR__ . '/../api/penduduk/verifikasi.php';
|
||||
$src = file_get_contents($file);
|
||||
|
||||
if ($src === false) {
|
||||
echo "FAIL: cannot read api/penduduk/verifikasi.php\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (strpos($src, "bind_param('sisi'") === false) {
|
||||
echo "FAIL: verifikasi bind_param must use 'sisi' for status, admin_id, catatan, id\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "PASS: verifikasi bind_param order is correct\n";
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$usersPage = file_get_contents($root . '/pages/users.php');
|
||||
$userCreate = file_get_contents($root . '/api/users/simpan.php');
|
||||
$userUpdate = file_get_contents($root . '/api/users/update.php');
|
||||
|
||||
check('users page does not offer enabled viewer role option', strpos($usersPage, '<option value="viewer">Viewer</option>') === false);
|
||||
check('users page keeps disabled legacy viewer option for existing accounts', strpos($usersPage, '<option value="viewer" disabled>Viewer') !== false);
|
||||
check('new account modal defaults to operator, not viewer', strpos($usersPage, "document.getElementById('fRole').value = isEdit ? user.role : 'operator';") !== false);
|
||||
check('backend still accepts viewer role for compatibility', strpos($userCreate, "'viewer'") !== false && strpos($userUpdate, "'viewer'") !== false);
|
||||
check('public map access remains unauthenticated', strpos(file_get_contents($root . '/index.php'), "\$role = is_logged_in() ? get_role() : 'viewer';") !== false);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$penduduk = file_get_contents($root . '/api/penduduk/ambil.php');
|
||||
$stats = file_get_contents($root . '/api/stats/ambil.php');
|
||||
$index = file_get_contents($root . '/index.php');
|
||||
$heatmap = file_get_contents($root . '/modules/heatmap.js');
|
||||
|
||||
check(
|
||||
'penduduk defines privileged role gate',
|
||||
strpos($penduduk, '$is_privileged = $is_authenticated && has_role(\'operator\');') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'penduduk public verification filter depends on privileged role',
|
||||
strpos($penduduk, '$public_verif_sql = $is_privileged ? "" : " AND pm.status_verifikasi = \'Terverifikasi\'";') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'penduduk sensitive data still requires operator role',
|
||||
strpos($penduduk, '$can_see_sensitive = $is_privileged;') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'stats defines privileged role gate',
|
||||
strpos($stats, '$is_privileged = $is_auth && has_role(\'operator\');') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'stats public verification filter depends on privileged role',
|
||||
strpos($stats, '$public_verif_sql = $is_privileged ? "" : " AND status_verifikasi = \'Terverifikasi\'";') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'public heatmap section is rendered only for admin or operator',
|
||||
strpos($index, '<?php if ($is_admin || $is_op): ?>') !== false
|
||||
&& strpos($index, 'Heatmap Kepadatan') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'public viewer does not render penduduk reachability layer chips',
|
||||
strpos($index, '<?php if ($is_admin || $is_op): ?>' . "\r\n" . ' <label class="layer-chip" title="Sembunyikan warga terjangkau (hijau)">') !== false
|
||||
|| strpos($index, '<?php if ($is_admin || $is_op): ?>' . "\n" . ' <label class="layer-chip" title="Sembunyikan warga terjangkau (hijau)">') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'public viewer does not render penduduk reachability dropdown',
|
||||
strpos($index, '<?php if ($is_admin || $is_op): ?>' . "\r\n" . ' <select id="filterKeterjangkauan"') !== false
|
||||
|| strpos($index, '<?php if ($is_admin || $is_op): ?>' . "\n" . ' <select id="filterKeterjangkauan"') !== false
|
||||
);
|
||||
|
||||
check(
|
||||
'heatmap module guards against public viewer activation',
|
||||
strpos($heatmap, '!window.APP_USER?.isOp') !== false
|
||||
&& strpos($heatmap, '!window.APP_USER?.isAdmin') !== false
|
||||
);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
header('Content-Type: text/plain');
|
||||
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$pass = 0;
|
||||
$fail = 0;
|
||||
|
||||
function check($label, $ok) {
|
||||
global $pass, $fail;
|
||||
if ($ok) {
|
||||
echo "PASS: {$label}\n";
|
||||
$pass++;
|
||||
} else {
|
||||
echo "FAIL: {$label}\n";
|
||||
$fail++;
|
||||
}
|
||||
}
|
||||
|
||||
$htaccess = $root . '/.htaccess';
|
||||
check('.htaccess exists at project root', file_exists($htaccess));
|
||||
|
||||
$content = file_exists($htaccess) ? file_get_contents($htaccess) : '';
|
||||
check(
|
||||
'.htaccess blocks tests directory',
|
||||
strpos($content, 'RedirectMatch 403 ^/webgis/WebgisPovertyMapping/tests(/|$)') !== false
|
||||
|| strpos($content, 'RewriteRule ^tests(/|$) - [F,L]') !== false
|
||||
);
|
||||
check('.htaccess blocks tmp test sessions', strpos($content, 'RewriteRule ^tmp(/|$) - [F,L]') !== false);
|
||||
|
||||
$deploy = file_get_contents($root . '/docs/deployment-xampp.md');
|
||||
check(
|
||||
'deployment docs mention blocking tests directory',
|
||||
strpos($deploy, 'Blokir folder tests') !== false
|
||||
|| strpos($deploy, 'block tests') !== false
|
||||
);
|
||||
|
||||
echo "\n--- {$pass} passed, {$fail} failed ---\n";
|
||||
exit($fail > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user