clock = new TestClock('2025-08-04 18:39:00');
$this->mlEngine = new TestMachineLearningEngine();
$this->repository = new InMemoryFeedbackRepository();
$this->logger = $this->createMock(Logger::class);
$this->feedbackService = new FeedbackService(
$this->repository,
$this->clock,
$this->logger
);
$this->learningService = new FeedbackLearningService(
$this->repository,
$this->mlEngine,
$this->clock,
$this->logger,
3, // Lower threshold for testing
0.5 // Higher learning rate for testing
);
// Set up ML engine to return success for adjustments
$this->mlEngine->withApplyFeedbackAdjustmentsResult([
'success' => true,
'applied_count' => 0,
'failed_count' => 0,
'results' => [],
]);
}
/**
* Test the complete feedback loop:
* 1. Submit feedback
* 2. Learn from feedback
* 3. Apply model adjustments
*/
public function testCompleteFeedbackLoop(): void
{
// 1. Submit feedback
$this->submitTestFeedback();
// Verify feedback was stored
$this->assertCount(5, $this->repository->getAllFeedback());
// 2. Learn from feedback
$learningResult = $this->learningService->learnFromFeedback();
// Verify learning result
$this->assertTrue($learningResult['success']);
$this->assertGreaterThan(0, $learningResult['total_adjustments_applied']);
// 3. Verify model adjustments were applied
$receivedAdjustments = $this->mlEngine->getReceivedAdjustments();
$this->assertNotEmpty($receivedAdjustments);
// Verify SQL_INJECTION adjustments
$sqlInjectionAdjustment = $this->findAdjustmentForCategory($receivedAdjustments, DetectionCategory::SQL_INJECTION);
$this->assertNotNull($sqlInjectionAdjustment);
// For false positives, threshold should be increased (positive adjustment)
$this->assertGreaterThan(0, $sqlInjectionAdjustment->thresholdAdjustment->getValue());
// For false positives, confidence should be decreased (negative adjustment)
$this->assertLessThan(0, $sqlInjectionAdjustment->confidenceAdjustment->getValue());
}
/**
* Test that severity adjustments are properly processed
*/
public function testSeverityAdjustments(): void
{
// Submit severity adjustment feedback
$this->feedbackService->submitSeverityAdjustment(
'detection1',
'test-user',
'This should be higher severity',
DetectionCategory::PATH_TRAVERSAL,
DetectionSeverity::MEDIUM,
DetectionSeverity::HIGH,
['test' => true]
);
$this->feedbackService->submitSeverityAdjustment(
'detection2',
'test-user',
'This should be higher severity',
DetectionCategory::PATH_TRAVERSAL,
DetectionSeverity::MEDIUM,
DetectionSeverity::HIGH,
['test' => true]
);
$this->feedbackService->submitSeverityAdjustment(
'detection3',
'test-user',
'This should be higher severity',
DetectionCategory::PATH_TRAVERSAL,
DetectionSeverity::MEDIUM,
DetectionSeverity::HIGH,
['test' => true]
);
// Learn from feedback
$learningResult = $this->learningService->learnFromFeedback();
// Verify learning result
$this->assertTrue($learningResult['success']);
// Verify model adjustments were applied
$receivedAdjustments = $this->mlEngine->getReceivedAdjustments();
// Verify PATH_TRAVERSAL adjustments
$pathTraversalAdjustment = $this->findAdjustmentForCategory($receivedAdjustments, DetectionCategory::PATH_TRAVERSAL);
$this->assertNotNull($pathTraversalAdjustment);
// For severity increase, confidence should be increased (positive adjustment)
$this->assertGreaterThan(0, $pathTraversalAdjustment->confidenceAdjustment->getValue());
// For severity adjustments, threshold should not be adjusted
$this->assertEquals(0, $pathTraversalAdjustment->thresholdAdjustment->getValue());
}
/**
* Test that false negative feedback is properly processed
*/
public function testFalseNegativeFeedback(): void
{
// Submit false negative feedback
$this->feedbackService->submitFalseNegative(
'detection1',
'test-user',
'This should have been detected',
DetectionCategory::COMMAND_INJECTION,
DetectionSeverity::HIGH,
['test' => true]
);
$this->feedbackService->submitFalseNegative(
'detection2',
'test-user',
'This should have been detected',
DetectionCategory::COMMAND_INJECTION,
DetectionSeverity::HIGH,
['test' => true]
);
// Learn from feedback
$learningResult = $this->learningService->learnFromFeedback();
// Verify learning result
$this->assertTrue($learningResult['success']);
// Verify model adjustments were applied
$receivedAdjustments = $this->mlEngine->getReceivedAdjustments();
// Verify COMMAND_INJECTION adjustments
$commandInjectionAdjustment = $this->findAdjustmentForCategory($receivedAdjustments, DetectionCategory::COMMAND_INJECTION);
$this->assertNotNull($commandInjectionAdjustment);
// For false negatives, threshold should be decreased (negative adjustment)
$this->assertLessThan(0, $commandInjectionAdjustment->thresholdAdjustment->getValue());
// For false negatives, confidence should be increased (positive adjustment)
$this->assertGreaterThan(0, $commandInjectionAdjustment->confidenceAdjustment->getValue());
}
/**
* Submit test feedback for various scenarios
*/
private function submitTestFeedback(): void
{
// Submit false positive feedback for SQL_INJECTION
$this->feedbackService->submitFalsePositive(
'detection1',
'test-user',
'This is a legitimate query',
DetectionCategory::SQL_INJECTION,
DetectionSeverity::HIGH,
['query' => 'SELECT * FROM users WHERE id = 1']
);
$this->feedbackService->submitFalsePositive(
'detection2',
'test-user',
'Another legitimate query',
DetectionCategory::SQL_INJECTION,
DetectionSeverity::HIGH,
['query' => 'SELECT * FROM products WHERE category_id = 2']
);
$this->feedbackService->submitFalsePositive(
'detection3',
'test-user',
'Yet another legitimate query',
DetectionCategory::SQL_INJECTION,
DetectionSeverity::HIGH,
['query' => 'SELECT * FROM orders WHERE customer_id = 3']
);
// Submit correct detection feedback
$this->feedbackService->submitCorrectDetection(
'detection4',
'test-user',
'This is indeed an XSS attempt',
DetectionCategory::XSS,
DetectionSeverity::CRITICAL,
['payload' => '']
);
// Submit false negative feedback
$this->feedbackService->submitFalseNegative(
'detection5',
'test-user',
'This should have been detected as XSS',
DetectionCategory::XSS,
DetectionSeverity::HIGH,
['payload' => '
']
);
}
/**
* Find an adjustment for a specific category in an array of adjustments
*
* @param array $adjustments Array of adjustments
* @param DetectionCategory $category Category to find
* @return ModelAdjustment|null The found adjustment or null
*/
private function findAdjustmentForCategory(array $adjustments, DetectionCategory $category): ?ModelAdjustment
{
foreach ($adjustments as $adjustment) {
if ($adjustment->category === $category) {
return $adjustment;
}
}
return null;
}
}