POSA_LEAKSMS/tests/threshold_sms_test.php
유창욱 90f121e14c chore: import codebase with security hardening
SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트).
전체 코드리뷰에서 도출된 보안 하드닝 10건 반영:
- 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시)
- 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php)
- 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점)
- CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php)
- 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화

_backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
2026-06-20 09:37:40 +09:00

86 lines
5 KiB
PHP

<?php
declare(strict_types=1);
// 온습도 임계 경보 판정 로직 회귀 테스트 (DB/네트워크 없이 순수 로직만).
// - evaluate_metric_thresholds(): 고온/저온/고습/저습 판정 + 대표 상태
// - metric_is_recovered(): 히스테리시스 적용 정상복귀 판정
// - metric_value_str(): 표시용 숫자 포맷
// 임계값은 config.php 의 METRIC_* 상수를 그대로 사용하며, 어떤 운영 임계 설정에서도
// 의미가 유지되도록 상대값(임계±여유, 중간값)으로 검증한다.
$root = dirname(__DIR__);
require_once $root . '/php/sms_send.php'; // config.php + 판정 함수 포함
$failures = [];
function check(bool $cond, string $msg): void {
global $failures;
if (!$cond) { $failures[] = $msg; }
}
function eq($a, $b, string $msg): void {
check($a === $b, $msg . ' (expected ' . var_export($b, true) . ', got ' . var_export($a, true) . ')');
}
// ── 함수 존재 ───────────────────────────────────────────────
check(function_exists('evaluate_metric_thresholds'), 'evaluate_metric_thresholds() should exist.');
check(function_exists('metric_is_recovered'), 'metric_is_recovered() should exist.');
check(function_exists('metric_value_str'), 'metric_value_str() should exist.');
$TH = (float)METRIC_TEMP_HIGH_C; $TL = (float)METRIC_TEMP_LOW_C;
$HH = (float)METRIC_RH_HIGH; $HL = (float)METRIC_RH_LOW;
check($TL < $TH, 'temperature thresholds sane (low < high).');
check($HL < $HH, 'humidity thresholds sane (low < high).');
$tMid = ($TL + $TH) / 2.0;
$hMid = ($HL + $HH) / 2.0;
// ── evaluate_metric_thresholds ──────────────────────────────
eq(evaluate_metric_thresholds($tMid, $hMid)['breaches'], [], 'normal temp+humidity -> no breaches');
eq(evaluate_metric_thresholds($tMid, $hMid)['status'], 'normal', 'normal -> status normal');
eq(evaluate_metric_thresholds($TH + 5, $hMid)['breaches'], ['high_temp'], 'above high temp -> high_temp');
eq(evaluate_metric_thresholds($TL - 5, $hMid)['breaches'], ['low_temp'], 'below low temp -> low_temp');
eq(evaluate_metric_thresholds($tMid, $HH + 5)['breaches'], ['high_humidity'], 'above high rh -> high_humidity');
eq(evaluate_metric_thresholds($tMid, $HL - 5)['breaches'], ['low_humidity'], 'below low rh -> low_humidity');
// 경계값 포함(>= / <=)
eq(evaluate_metric_thresholds($TH, $hMid)['breaches'], ['high_temp'], 'temp == high threshold -> high_temp (inclusive)');
eq(evaluate_metric_thresholds($TL, $hMid)['breaches'], ['low_temp'], 'temp == low threshold -> low_temp (inclusive)');
eq(evaluate_metric_thresholds($tMid, $HH)['breaches'], ['high_humidity'], 'rh == high threshold -> high_humidity (inclusive)');
eq(evaluate_metric_thresholds($tMid, $HL)['breaches'], ['low_humidity'], 'rh == low threshold -> low_humidity (inclusive)');
// 동시 위반 + 대표 상태(첫 breach)
eq(evaluate_metric_thresholds($TH + 5, $HH + 5)['breaches'], ['high_temp', 'high_humidity'], 'high temp+humidity -> both breaches');
eq(evaluate_metric_thresholds($TH + 5, $HH + 5)['status'], 'high_temp', 'status = first breach');
// 결측(null) 처리
eq(evaluate_metric_thresholds(null, $HH + 5)['breaches'], ['high_humidity'], 'null temp + high rh -> high_humidity only');
eq(evaluate_metric_thresholds($TH + 5, null)['breaches'], ['high_temp'], 'high temp + null rh -> high_temp only');
eq(evaluate_metric_thresholds(null, null)['breaches'], [], 'all null -> no breaches');
// ── metric_is_recovered (히스테리시스) ──────────────────────
check(metric_is_recovered($tMid, $hMid) === true, 'mid values are recovered');
check(metric_is_recovered($TH + 5, $hMid) === false, 'above high temp not recovered');
check(metric_is_recovered($tMid, $HH + 5) === false, 'above high rh not recovered');
// 임계 바로 안쪽(히스테리시스 데드밴드)은 복구로 보지 않음
check(metric_is_recovered($TH - ((float)METRIC_TEMP_HYSTERESIS_C / 2.0), $hMid) === false, 'temp inside hysteresis deadband not recovered');
check(metric_is_recovered($tMid, $HH - ((float)METRIC_RH_HYSTERESIS / 2.0)) === false, 'rh inside hysteresis deadband not recovered');
// 결측은 해당 지표 정상 간주
check(metric_is_recovered(null, $hMid) === true, 'null temp recovered when rh normal');
check(metric_is_recovered($tMid, null) === true, 'null rh recovered when temp normal');
// ── metric_value_str ────────────────────────────────────────
eq(metric_value_str(30.0), '30', '30.0 -> 30');
eq(metric_value_str(25.5), '25.5', '25.5 -> 25.5');
eq(metric_value_str(-3.0), '-3', '-3.0 -> -3');
eq(metric_value_str(100.0), '100', '100.0 -> 100');
eq(metric_value_str(0.0), '0', '0.0 -> 0');
if ($failures) {
fwrite(STDERR, "Threshold SMS logic test failures:\n");
foreach ($failures as $f) {
fwrite(STDERR, "- {$f}\n");
}
exit(1);
}
echo "Threshold SMS logic checks passed.\n";