steps(); $this->assertCount(6, $steps); $this->assertSame('beauftragung', $steps[0]['key']); $this->assertSame(SecurityCheckProcess::STEP_PERFORM_CHECK, $steps[4]['key']); $this->assertSame('report', $steps[5]['key']); } public function testManualStepKeysExcludeTechChecklistStep(): void { $keys = (new SecurityCheckProcess())->manualStepKeys(); $this->assertCount(5, $keys); $this->assertNotContains(SecurityCheckProcess::STEP_PERFORM_CHECK, $keys); $this->assertContains(SecurityCheckProcess::STEP_INFORMATION, $keys); } public function testInfoStepFieldsAreDefined(): void { $fields = (new SecurityCheckProcess())->stepFields(SecurityCheckProcess::STEP_INFORMATION); $this->assertNotEmpty($fields); $keys = array_column($fields, 'key'); $this->assertContains('technical_contact', $keys); $this->assertContains('external_systems', $keys); } public function testRequiredFieldKeysExcludeOptionalFields(): void { $keys = (new SecurityCheckProcess())->requiredFieldKeys(SecurityCheckProcess::STEP_INFORMATION); $this->assertContains('technical_contact', $keys); $this->assertContains('deployment', $keys); $this->assertContains('system_info', $keys); $this->assertContains('external_systems', $keys); $this->assertNotContains('special_notes', $keys); } public function testSchedulingAndBlockerStepsRequireTwoDatetimeFields(): void { $process = new SecurityCheckProcess(); $expected = [ 'scheduling' => ['golive_start', 'golive_end'], 'blocker_appointment' => ['blocker_start', 'blocker_end'], ]; foreach ($expected as $stepKey => $expectedKeys) { $fields = $process->stepFields($stepKey); $this->assertCount(2, $fields); foreach ($fields as $field) { $this->assertSame(SecurityCheckProcess::FIELD_DATETIME, $field['type']); $this->assertTrue($field['required']); } $this->assertSame($expectedKeys, $process->requiredFieldKeys($stepKey)); } } public function testTechCheckItemsExtractsOnlyCheckItemsWithKeys(): void { $snapshot = ['items' => [ ['type' => 'section', 'label' => 'Group'], ['type' => 'check', 'key' => 'tls', 'label' => 'TLS'], ['type' => 'check', 'key' => '', 'label' => 'No key'], ['type' => 'check', 'label' => 'Missing key'], 'not-an-array', ]]; $items = SecurityCheckProcess::techCheckItems($snapshot); $this->assertCount(1, $items); $this->assertSame('tls', $items[0]['key']); } public function testComputeProgressOpenWhenNothingDone(): void { $progress = (new SecurityCheckProcess())->computeProgress([], [], []); $this->assertSame(5, $progress['manual_total']); $this->assertSame(0, $progress['tech_total']); $this->assertSame(0, $progress['done']); $this->assertSame(0, $progress['percent']); $this->assertSame(SecurityCheckProcess::STATUS_OPEN, $progress['status']); $this->assertTrue($progress['step6_done']); // no tech items → trivially done } public function testComputeProgressInProgressWhenPartiallyDone(): void { $processState = ['beauftragung' => ['done' => true]]; $progress = (new SecurityCheckProcess())->computeProgress($processState, [], []); $this->assertSame(1, $progress['done']); $this->assertSame(SecurityCheckProcess::STATUS_IN_PROGRESS, $progress['status']); } public function testComputeProgressCompletedWhenAllDone(): void { $process = new SecurityCheckProcess(); $snapshot = ['items' => [ ['type' => 'check', 'key' => 'a', 'label' => 'A'], ['type' => 'check', 'key' => 'b', 'label' => 'B'], ]]; $processState = []; foreach ($process->manualStepKeys() as $key) { $processState[$key] = ['done' => true]; } $techState = ['a' => ['done' => true], 'b' => ['done' => true]]; $progress = $process->computeProgress($processState, $snapshot, $techState); $this->assertSame(7, $progress['total']); // 5 manual + 2 tech $this->assertSame(7, $progress['done']); $this->assertSame(100, $progress['percent']); $this->assertTrue($progress['step6_done']); $this->assertSame(SecurityCheckProcess::STATUS_COMPLETED, $progress['status']); } public function testComputeProgressStep6NotDoneWhenSomeTechItemsOpen(): void { $snapshot = ['items' => [ ['type' => 'check', 'key' => 'a', 'label' => 'A'], ['type' => 'check', 'key' => 'b', 'label' => 'B'], ]]; $techState = ['a' => ['done' => true]]; $progress = (new SecurityCheckProcess())->computeProgress([], $snapshot, $techState); $this->assertFalse($progress['step6_done']); $this->assertSame(SecurityCheckProcess::STATUS_IN_PROGRESS, $progress['status']); } public function testComputeProgressArchivedOverridesDerivedStatus(): void { $progress = (new SecurityCheckProcess())->computeProgress([], [], [], true); $this->assertSame(SecurityCheckProcess::STATUS_ARCHIVED, $progress['status']); } public function testReportPrerequisitesMetWhenAllPrecedingStepsComplete(): void { $process = new SecurityCheckProcess(); $snapshot = ['items' => [['type' => 'check', 'key' => 'a', 'label' => 'A']]]; $processState = []; foreach ($process->manualStepKeys() as $key) { // The report step itself is intentionally left open — it does not gate itself. $processState[$key] = ['done' => $key !== SecurityCheckProcess::STEP_REPORT]; } $techState = ['a' => ['done' => true]]; $this->assertTrue($process->reportPrerequisitesMet($processState, $snapshot, $techState)); } public function testReportPrerequisitesNotMetWhenAManualStepIsOpen(): void { $process = new SecurityCheckProcess(); $snapshot = ['items' => [['type' => 'check', 'key' => 'a', 'label' => 'A']]]; $processState = []; foreach ($process->manualStepKeys() as $key) { $processState[$key] = ['done' => true]; } $processState['scheduling']['done'] = false; // one preceding step still open $techState = ['a' => ['done' => true]]; $this->assertFalse($process->reportPrerequisitesMet($processState, $snapshot, $techState)); } public function testReportPrerequisitesNotMetWhenTechChecklistIncomplete(): void { $process = new SecurityCheckProcess(); $snapshot = ['items' => [ ['type' => 'check', 'key' => 'a', 'label' => 'A'], ['type' => 'check', 'key' => 'b', 'label' => 'B'], ]]; $processState = []; foreach ($process->manualStepKeys() as $key) { $processState[$key] = ['done' => true]; } $techState = ['a' => ['done' => true]]; // 'b' still open $this->assertFalse($process->reportPrerequisitesMet($processState, $snapshot, $techState)); } public function testReportPrerequisitesMetWithEmptyTechChecklist(): void { $process = new SecurityCheckProcess(); $processState = []; foreach ($process->manualStepKeys() as $key) { $processState[$key] = ['done' => true]; } // No tech items → checklist trivially complete; only the manual steps gate. $this->assertTrue($process->reportPrerequisitesMet($processState, [], [])); } public function testStatusVariantMapping(): void { $this->assertSame('neutral', SecurityCheckProcess::statusVariant(SecurityCheckProcess::STATUS_OPEN)); $this->assertSame('warning', SecurityCheckProcess::statusVariant(SecurityCheckProcess::STATUS_IN_PROGRESS)); $this->assertSame('success', SecurityCheckProcess::statusVariant(SecurityCheckProcess::STATUS_COMPLETED)); $this->assertSame('neutral', SecurityCheckProcess::statusVariant(SecurityCheckProcess::STATUS_ARCHIVED)); } }