feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
253
tests/Unit/Framework/Analytics/AnalyticsCollectorTest.php
Normal file
253
tests/Unit/Framework/Analytics/AnalyticsCollectorTest.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Analytics\AnalyticsCategory;
|
||||
use App\Framework\Analytics\AnalyticsCollector;
|
||||
use App\Framework\Analytics\Storage\AnalyticsStorage;
|
||||
use App\Framework\Http\ServerEnvironment;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
|
||||
describe('AnalyticsCollector', function () {
|
||||
beforeEach(function () {
|
||||
// Mock dependencies
|
||||
$this->performanceCollector = Mockery::mock(PerformanceCollectorInterface::class);
|
||||
$this->storage = Mockery::mock(AnalyticsStorage::class);
|
||||
$this->random = Mockery::mock(RandomGenerator::class);
|
||||
|
||||
// Create real ServerEnvironment with test data (final class, can't be mocked)
|
||||
$this->serverEnvironment = new ServerEnvironment([
|
||||
'REMOTE_ADDR' => '127.0.0.1',
|
||||
'HTTP_USER_AGENT' => 'Test-Agent/1.0',
|
||||
'REQUEST_URI' => '/test',
|
||||
'HTTP_REFERER' => 'https://example.com',
|
||||
]);
|
||||
|
||||
// Allow any performance collector calls (framework internal)
|
||||
$this->performanceCollector
|
||||
->shouldReceive('recordMetric')
|
||||
->zeroOrMoreTimes();
|
||||
|
||||
$this->performanceCollector
|
||||
->shouldReceive('increment')
|
||||
->zeroOrMoreTimes();
|
||||
|
||||
// Allow storage aggregated calls (framework internal)
|
||||
$this->storage
|
||||
->shouldReceive('storeAggregated')
|
||||
->zeroOrMoreTimes();
|
||||
|
||||
// Allow random float calls for sampling (may or may not be called)
|
||||
$this->random
|
||||
->shouldReceive('float')
|
||||
->with(0, 1)
|
||||
->zeroOrMoreTimes()
|
||||
->andReturn(0.5);
|
||||
|
||||
// Allow random bytes calls for session ID generation (may or may not be called)
|
||||
$this->random
|
||||
->shouldReceive('bytes')
|
||||
->with(16)
|
||||
->zeroOrMoreTimes()
|
||||
->andReturn(str_repeat('a', 16));
|
||||
|
||||
// Default: tracking enabled, 100% sampling for tests
|
||||
$this->collector = new AnalyticsCollector(
|
||||
performanceCollector: $this->performanceCollector,
|
||||
storage: $this->storage,
|
||||
random: $this->random,
|
||||
serverEnvironment: $this->serverEnvironment,
|
||||
enabled: true,
|
||||
samplingRate: 1.0
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
describe('trackAction', function () {
|
||||
it('tracks user action with category and properties', function () {
|
||||
// Expect raw data storage with flexible array matcher
|
||||
$this->storage
|
||||
->shouldReceive('storeRawData')
|
||||
->once()
|
||||
->with(
|
||||
Mockery::on(function ($data) {
|
||||
return is_array($data)
|
||||
&& $data['category'] === 'user_behavior'
|
||||
&& $data['action'] === 'button_click'
|
||||
&& isset($data['session_id'])
|
||||
&& isset($data['timestamp'])
|
||||
&& isset($data['button_id'])
|
||||
&& $data['button_id'] === 'submit-btn';
|
||||
}),
|
||||
1.0
|
||||
);
|
||||
|
||||
// Track action
|
||||
$this->collector->trackAction(
|
||||
action: 'button_click',
|
||||
category: AnalyticsCategory::USER_BEHAVIOR,
|
||||
properties: ['button_id' => 'submit-btn']
|
||||
);
|
||||
});
|
||||
|
||||
it('does not track when analytics disabled', function () {
|
||||
// Create collector with analytics disabled
|
||||
$disabledCollector = new AnalyticsCollector(
|
||||
performanceCollector: $this->performanceCollector,
|
||||
storage: $this->storage,
|
||||
random: $this->random,
|
||||
serverEnvironment: $this->serverEnvironment,
|
||||
enabled: false, // Disabled
|
||||
samplingRate: 1.0
|
||||
);
|
||||
|
||||
// Storage should NOT be called
|
||||
$this->storage->shouldNotReceive('storeRawData');
|
||||
|
||||
// Track action (should be ignored)
|
||||
$disabledCollector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR);
|
||||
});
|
||||
|
||||
it('respects sampling rate', function () {
|
||||
// Create new Random mock for this test
|
||||
$randomMock = Mockery::mock(RandomGenerator::class);
|
||||
|
||||
// Random returns 0.6 (above 0.5 threshold) -> should NOT track (0.6 > 0.5)
|
||||
$randomMock->shouldReceive('float')->with(0, 1)->andReturn(0.6);
|
||||
|
||||
// Create collector with 50% sampling
|
||||
$sampledCollector = new AnalyticsCollector(
|
||||
performanceCollector: $this->performanceCollector,
|
||||
storage: $this->storage,
|
||||
random: $randomMock,
|
||||
serverEnvironment: $this->serverEnvironment,
|
||||
enabled: true,
|
||||
samplingRate: 0.5
|
||||
);
|
||||
|
||||
// Storage should NOT be called (sampled out)
|
||||
$this->storage->shouldNotReceive('storeRawData');
|
||||
|
||||
// Track action (should be sampled out)
|
||||
$sampledCollector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackPageView', function () {
|
||||
it('tracks page view with path and title', function () {
|
||||
// Expect raw data storage with flexible matcher
|
||||
$this->storage
|
||||
->shouldReceive('storeRawData')
|
||||
->once()
|
||||
->with(
|
||||
Mockery::on(function ($data) {
|
||||
return is_array($data)
|
||||
&& $data['path'] === '/dashboard'
|
||||
&& $data['title'] === 'Dashboard'
|
||||
&& isset($data['timestamp'])
|
||||
&& isset($data['session_id']);
|
||||
}),
|
||||
1.0
|
||||
);
|
||||
|
||||
// Track page view
|
||||
$this->collector->trackPageView(
|
||||
path: '/dashboard',
|
||||
title: 'Dashboard'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackError', function () {
|
||||
it('tracks error with type and message', function () {
|
||||
// trackError only logs to performance collector, not storage
|
||||
// Storage expectations are handled by global mocks
|
||||
|
||||
// Track error
|
||||
$this->collector->trackError(
|
||||
errorType: 'ValidationException',
|
||||
message: 'Invalid email format'
|
||||
);
|
||||
|
||||
// Test passes if no exceptions are thrown
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackBusinessEvent', function () {
|
||||
it('tracks business event with value and currency', function () {
|
||||
// trackBusinessEvent only logs to performance collector, not storage
|
||||
// Storage expectations are handled by global mocks
|
||||
|
||||
// Track business event
|
||||
$this->collector->trackBusinessEvent(
|
||||
event: 'purchase_completed',
|
||||
value: 99.99,
|
||||
currency: 'EUR'
|
||||
);
|
||||
|
||||
// Test passes if no exceptions are thrown
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackApiCall', function () {
|
||||
it('tracks API call with endpoint and metrics', function () {
|
||||
// trackApiCall only logs to performance collector, not storage
|
||||
// Storage expectations are handled by global mocks
|
||||
|
||||
// Track API call
|
||||
$this->collector->trackApiCall(
|
||||
endpoint: '/api/users',
|
||||
method: 'GET',
|
||||
responseCode: 200,
|
||||
responseTime: 0.125
|
||||
);
|
||||
|
||||
// Test passes if no exceptions are thrown
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', function () {
|
||||
it('handles zero sampling rate', function () {
|
||||
// Create new Random mock for this test
|
||||
$randomMock = Mockery::mock(RandomGenerator::class);
|
||||
|
||||
// Random float will be called and return value > 0.0 (will fail sampling)
|
||||
$randomMock->shouldReceive('float')->with(0, 1)->andReturn(0.1);
|
||||
|
||||
// Create collector with 0% sampling (no tracking)
|
||||
$noSamplingCollector = new AnalyticsCollector(
|
||||
performanceCollector: $this->performanceCollector,
|
||||
storage: $this->storage,
|
||||
random: $randomMock,
|
||||
serverEnvironment: $this->serverEnvironment,
|
||||
enabled: true,
|
||||
samplingRate: 0.0
|
||||
);
|
||||
|
||||
// Storage should NOT be called
|
||||
$this->storage->shouldNotReceive('storeRawData');
|
||||
|
||||
// Track action (should be sampled out)
|
||||
$noSamplingCollector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR);
|
||||
});
|
||||
|
||||
it('handles full sampling rate', function () {
|
||||
// With 100% sampling, float() should NOT be called (early return)
|
||||
// Expect storage to be called
|
||||
$this->storage
|
||||
->shouldReceive('storeRawData')
|
||||
->once()
|
||||
->with(Mockery::type('array'), 1.0);
|
||||
|
||||
// Track action (should be tracked)
|
||||
$this->collector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user