Files
michaelschiemer/tests/Unit/Framework/Analytics/AnalyticsCollectorTest.php

254 lines
9.2 KiB
PHP

<?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);
});
});
});