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 제외.
86 lines
5 KiB
PHP
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";
|