repository = $this->createMock(FeedbackRepositoryInterface::class); $this->mlEngine = new TestMachineLearningEngine(); $this->clock = new TestClock('2025-08-04 18:39:00'); $this->logger = $this->createMock(Logger::class); $this->service = new FeedbackLearningService( $this->repository, $this->mlEngine, $this->clock, $this->logger, 5, // minimumFeedbackThreshold 0.3 // learningRate ); } public function testLearnFromFeedbackWithInsufficientData(): void { // Set up repository to return stats with insufficient feedback $this->repository->method('getFeedbackStats')->willReturn([ 'total_count' => 3, // Less than minimum threshold of 5 ]); // Execute the learning process $result = $this->service->learnFromFeedback(); // Verify the result $this->assertFalse($result['success']); $this->assertEquals('Not enough feedback for learning', $result['message']); $this->assertEquals(3, $result['feedback_count']); $this->assertEquals(5, $result['minimum_threshold']); } public function testLearnFromFeedbackWithSufficientData(): void { // Set up repository to return stats with sufficient feedback $this->repository->method('getFeedbackStats')->willReturn([ 'total_count' => 10, ]); // Set up repository to return feedback data $falsePositives = [ $this->createFalsePositiveFeedback('detection1', DetectionCategory::SQL_INJECTION), $this->createFalsePositiveFeedback('detection2', DetectionCategory::SQL_INJECTION), $this->createFalsePositiveFeedback('detection3', DetectionCategory::SQL_INJECTION), $this->createFalsePositiveFeedback('detection4', DetectionCategory::XSS), ]; $falseNegatives = [ $this->createFalseNegativeFeedback('detection5', DetectionCategory::COMMAND_INJECTION), $this->createFalseNegativeFeedback('detection6', DetectionCategory::COMMAND_INJECTION), ]; $severityAdjustments = [ $this->createSeverityAdjustmentFeedback( 'detection7', DetectionCategory::PATH_TRAVERSAL, DetectionSeverity::MEDIUM, DetectionSeverity::HIGH ), $this->createSeverityAdjustmentFeedback( 'detection8', DetectionCategory::PATH_TRAVERSAL, DetectionSeverity::MEDIUM, DetectionSeverity::HIGH ), $this->createSeverityAdjustmentFeedback( 'detection9', DetectionCategory::PATH_TRAVERSAL, DetectionSeverity::MEDIUM, DetectionSeverity::HIGH ), ]; $this->repository->method('getFeedbackByFeedbackType') ->willReturnMap([ [FeedbackType::FALSE_POSITIVE, null, $falsePositives], [FeedbackType::FALSE_NEGATIVE, null, $falseNegatives], [FeedbackType::SEVERITY_ADJUSTMENT, null, $severityAdjustments], [FeedbackType::CORRECT_DETECTION, null, []], ]); // Set up ML engine to return success for adjustments $this->mlEngine->withApplyFeedbackAdjustmentsResult([ 'success' => true, 'applied_count' => 3, 'failed_count' => 0, 'results' => [], ]); // Execute the learning process $result = $this->service->learnFromFeedback(); // Verify the result $this->assertTrue($result['success']); $this->assertEquals(4, $result['false_positives_processed']); $this->assertEquals(2, $result['false_negatives_processed']); $this->assertEquals(3, $result['severity_adjustments_processed']); $this->assertGreaterThan(0, $result['total_adjustments_applied']); } public function testProcessFalsePositives(): void { // Create a service with reflection to access private methods $service = new FeedbackLearningService( $this->repository, $this->mlEngine, $this->clock, $this->logger, 5, 0.3 ); $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'processfalsePositives'); $reflectionMethod->setAccessible(true); // Create test data $falsePositives = [ $this->createFalsePositiveFeedback('detection1', DetectionCategory::SQL_INJECTION), $this->createFalsePositiveFeedback('detection2', DetectionCategory::SQL_INJECTION), $this->createFalsePositiveFeedback('detection3', DetectionCategory::SQL_INJECTION), $this->createFalsePositiveFeedback('detection4', DetectionCategory::XSS), ]; // Execute the method $adjustments = $reflectionMethod->invoke($service, $falsePositives); // Verify the adjustments $this->assertCount(2, $adjustments); // One for SQL_INJECTION, one for XSS // Check SQL_INJECTION adjustment $sqlInjectionAdjustment = null; $xssAdjustment = null; foreach ($adjustments as $adjustment) { if ($adjustment->category === DetectionCategory::SQL_INJECTION) { $sqlInjectionAdjustment = $adjustment; } elseif ($adjustment->category === DetectionCategory::XSS) { $xssAdjustment = $adjustment; } } $this->assertNotNull($sqlInjectionAdjustment); $this->assertNotNull($xssAdjustment); // SQL_INJECTION had 3 false positives, so should have stronger adjustment $this->assertGreaterThan( $xssAdjustment->thresholdAdjustment->getValue(), $sqlInjectionAdjustment->thresholdAdjustment->getValue() ); // Threshold adjustments should be positive for false positives (make detection harder) $this->assertGreaterThan(0, $sqlInjectionAdjustment->thresholdAdjustment->getValue()); $this->assertGreaterThan(0, $xssAdjustment->thresholdAdjustment->getValue()); // Confidence adjustments should be negative for false positives (less certain) $this->assertLessThan(0, $sqlInjectionAdjustment->confidenceAdjustment->getValue()); $this->assertLessThan(0, $xssAdjustment->confidenceAdjustment->getValue()); } public function testProcessFalseNegatives(): void { // Create a service with reflection to access private methods $service = new FeedbackLearningService( $this->repository, $this->mlEngine, $this->clock, $this->logger, 5, 0.3 ); $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'processFalseNegatives'); $reflectionMethod->setAccessible(true); // Create test data $falseNegatives = [ $this->createFalseNegativeFeedback('detection1', DetectionCategory::COMMAND_INJECTION), $this->createFalseNegativeFeedback('detection2', DetectionCategory::COMMAND_INJECTION), ]; // Execute the method $adjustments = $reflectionMethod->invoke($service, $falseNegatives); // Verify the adjustments $this->assertCount(1, $adjustments); // One for COMMAND_INJECTION $adjustment = reset($adjustments); // Threshold adjustments should be negative for false negatives (make detection easier) $this->assertLessThan(0, $adjustment->thresholdAdjustment->getValue()); // Confidence adjustments should be positive for false negatives (more certain) $this->assertGreaterThan(0, $adjustment->confidenceAdjustment->getValue()); } public function testProcessSeverityAdjustments(): void { // Create a service with reflection to access private methods $service = new FeedbackLearningService( $this->repository, $this->mlEngine, $this->clock, $this->logger, 5, 0.3 ); $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'processSeverityAdjustments'); $reflectionMethod->setAccessible(true); // Create test data $severityAdjustments = [ $this->createSeverityAdjustmentFeedback( 'detection1', DetectionCategory::PATH_TRAVERSAL, DetectionSeverity::MEDIUM, DetectionSeverity::HIGH ), $this->createSeverityAdjustmentFeedback( 'detection2', DetectionCategory::PATH_TRAVERSAL, DetectionSeverity::MEDIUM, DetectionSeverity::HIGH ), $this->createSeverityAdjustmentFeedback( 'detection3', DetectionCategory::PATH_TRAVERSAL, DetectionSeverity::MEDIUM, DetectionSeverity::HIGH ), ]; // Execute the method $result = $reflectionMethod->invoke($service, $severityAdjustments); // Verify the result $this->assertArrayHasKey('adjustments', $result); $this->assertArrayHasKey('severity_changes', $result); $adjustments = $result['adjustments']; $severityChanges = $result['severity_changes']; $this->assertCount(1, $adjustments); // One for PATH_TRAVERSAL $this->assertCount(1, $severityChanges); // One change from MEDIUM to HIGH $adjustment = reset($adjustments); // For severity increase, confidence adjustment should be positive $this->assertGreaterThan(0, $adjustment->confidenceAdjustment->getValue()); // Threshold should not be adjusted for severity changes $this->assertEquals(0, $adjustment->thresholdAdjustment->getValue()); } public function testGenerateFeatureWeightAdjustments(): void { // Create a service with reflection to access private methods $service = new FeedbackLearningService( $this->repository, $this->mlEngine, $this->clock, $this->logger, 5, 0.3 ); $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'generateFeatureWeightAdjustments'); $reflectionMethod->setAccessible(true); // Test for SQL_INJECTION false positive $sqlInjectionFalsePositiveAdjustments = $reflectionMethod->invoke( $service, DetectionCategory::SQL_INJECTION, true // isfalsePositive ); // Test for SQL_INJECTION false negative $sqlInjectionFalseNegativeAdjustments = $reflectionMethod->invoke( $service, DetectionCategory::SQL_INJECTION, false // isfalsePositive ); // Verify the adjustments $this->assertNotEmpty($sqlInjectionFalsePositiveAdjustments); $this->assertNotEmpty($sqlInjectionFalseNegativeAdjustments); // For false positives, adjustments should be negative (reduce feature importance) foreach ($sqlInjectionFalsePositiveAdjustments as $feature => $adjustment) { $this->assertLessThan(0, $adjustment); } // For false negatives, adjustments should be positive (increase feature importance) foreach ($sqlInjectionFalseNegativeAdjustments as $feature => $adjustment) { $this->assertGreaterThan(0, $adjustment); } } public function testApplyModelAdjustments(): void { // Create test data $adjustments = [ 'adjustment1' => new ModelAdjustment( 'adjustment1', DetectionCategory::SQL_INJECTION, Percentage::from(10.0), Percentage::from(-10.0), ['sql_keywords_count' => -0.2], 'Test adjustment 1', Timestamp::fromClock($this->clock) ), 'adjustment2' => new ModelAdjustment( 'adjustment2', DetectionCategory::XSS, Percentage::from(5.0), Percentage::from(-5.0), ['script_tag_count' => -0.3], 'Test adjustment 2', Timestamp::fromClock($this->clock) ), ]; // Set up ML engine to return success $this->mlEngine->withApplyFeedbackAdjustmentsResult([ 'success' => true, 'applied_count' => 2, 'failed_count' => 0, 'results' => [], ]); // Create a service with reflection to access private methods $service = new FeedbackLearningService( $this->repository, $this->mlEngine, $this->clock, $this->logger, 5, 0.3 ); $reflectionMethod = new \ReflectionMethod(FeedbackLearningService::class, 'applyModelAdjustments'); $reflectionMethod->setAccessible(true); // Execute the method $result = $reflectionMethod->invoke($service, $adjustments); // Verify the result $this->assertTrue($result['success']); $this->assertEquals(2, $result['applied_count']); $this->assertEquals(0, $result['failed_count']); // Verify that the correct adjustments were passed to the ML engine $this->assertSame($adjustments, $this->mlEngine->getReceivedAdjustments()); } public function testWithMinimumFeedbackThreshold(): void { $newService = $this->service->withMinimumFeedbackThreshold(10); $this->assertNotSame($this->service, $newService); $this->assertEquals(10, $newService->getMinimumFeedbackThreshold()); $this->assertEquals(5, $this->service->getMinimumFeedbackThreshold()); } public function testWithLearningRate(): void { $newService = $this->service->withLearningRate(0.5); $this->assertNotSame($this->service, $newService); $this->assertEquals(0.5, $newService->getLearningRate()); $this->assertEquals(0.3, $this->service->getLearningRate()); } /** * Helper method to create a false positive feedback */ private function createFalsePositiveFeedback( string $detectionId, DetectionCategory $category, DetectionSeverity $severity = DetectionSeverity::MEDIUM ): DetectionFeedback { return DetectionFeedback::falsePositive( $detectionId, 'test-user', 'This is a false positive', $category, $severity, ['test_context' => true] ); } /** * Helper method to create a false negative feedback */ private function createFalseNegativeFeedback( string $detectionId, DetectionCategory $category, DetectionSeverity $severity = DetectionSeverity::MEDIUM ): DetectionFeedback { return DetectionFeedback::falseNegative( $detectionId, 'test-user', 'This is a false negative', $category, $severity, ['test_context' => true] ); } /** * Helper method to create a severity adjustment feedback */ private function createSeverityAdjustmentFeedback( string $detectionId, DetectionCategory $category, DetectionSeverity $currentSeverity, DetectionSeverity $suggestedSeverity ): DetectionFeedback { return DetectionFeedback::severityAdjustment( $detectionId, 'test-user', 'This severity should be adjusted', $category, $currentSeverity, $suggestedSeverity, ['test_context' => true] ); } }