You've already forked wp-prometheus
feat: Add comprehensive PHPUnit test suite and CI/CD test gating (v0.5.0)
189 tests across 8 test classes covering all core plugin classes: CustomMetricBuilder, StorageFactory, Authentication, DashboardProvider, RuntimeCollector, Installer, Collector, and MetricsEndpoint. Added test job to Gitea release workflow that gates build-release. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
155
tests/Unit/Metrics/CollectorTest.php
Normal file
155
tests/Unit/Metrics/CollectorTest.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WpPrometheus\Tests\Unit\Metrics;
|
||||
|
||||
use Magdev\WpPrometheus\Metrics\Collector;
|
||||
use Magdev\WpPrometheus\Metrics\RuntimeCollector;
|
||||
use Magdev\WpPrometheus\Metrics\StorageFactory;
|
||||
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Prometheus\CollectorRegistry;
|
||||
use Prometheus\Counter;
|
||||
use Prometheus\Gauge;
|
||||
use Prometheus\Histogram;
|
||||
|
||||
#[CoversClass(Collector::class)]
|
||||
class CollectorTest extends TestCase
|
||||
{
|
||||
private Collector $collector;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->resetStorageFactory();
|
||||
$this->collector = new Collector();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->resetRuntimeCollectorSingleton();
|
||||
$this->resetStorageFactory();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
// ── Constructor & Basic Properties ─────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function constructor_creates_registry(): void
|
||||
{
|
||||
$this->assertInstanceOf(CollectorRegistry::class, $this->collector->get_registry());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_namespace_returns_wordpress(): void
|
||||
{
|
||||
$this->assertSame('wordpress', $this->collector->get_namespace());
|
||||
}
|
||||
|
||||
// ── register_gauge() ──────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function register_gauge_returns_gauge_instance(): void
|
||||
{
|
||||
$gauge = $this->collector->register_gauge('test_metric', 'A test gauge');
|
||||
$this->assertInstanceOf(Gauge::class, $gauge);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function register_gauge_with_labels(): void
|
||||
{
|
||||
$gauge = $this->collector->register_gauge(
|
||||
'labeled_metric',
|
||||
'A labeled gauge',
|
||||
['label1', 'label2']
|
||||
);
|
||||
$this->assertInstanceOf(Gauge::class, $gauge);
|
||||
}
|
||||
|
||||
// ── register_counter() ────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function register_counter_returns_counter_instance(): void
|
||||
{
|
||||
$counter = $this->collector->register_counter('test_counter', 'A test counter');
|
||||
$this->assertInstanceOf(Counter::class, $counter);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function register_counter_with_labels(): void
|
||||
{
|
||||
$counter = $this->collector->register_counter(
|
||||
'labeled_counter',
|
||||
'A labeled counter',
|
||||
['method', 'status']
|
||||
);
|
||||
$this->assertInstanceOf(Counter::class, $counter);
|
||||
}
|
||||
|
||||
// ── register_histogram() ──────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function register_histogram_returns_histogram_instance(): void
|
||||
{
|
||||
$histogram = $this->collector->register_histogram('test_histogram', 'A test histogram');
|
||||
$this->assertInstanceOf(Histogram::class, $histogram);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function register_histogram_with_custom_buckets(): void
|
||||
{
|
||||
$buckets = [0.1, 0.5, 1.0, 5.0];
|
||||
$histogram = $this->collector->register_histogram(
|
||||
'custom_buckets_hist',
|
||||
'A histogram with custom buckets',
|
||||
['label1'],
|
||||
$buckets
|
||||
);
|
||||
$this->assertInstanceOf(Histogram::class, $histogram);
|
||||
}
|
||||
|
||||
// ── render() ──────────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function render_returns_string(): void
|
||||
{
|
||||
$getOption = $this->getFunctionMock('Magdev\\WpPrometheus\\Metrics', 'get_option');
|
||||
$getOption->expects($this->any())->willReturn([]);
|
||||
|
||||
$output = $this->collector->render();
|
||||
$this->assertIsString($output);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function render_includes_registered_gauge_value(): void
|
||||
{
|
||||
$getOption = $this->getFunctionMock('Magdev\\WpPrometheus\\Metrics', 'get_option');
|
||||
$getOption->expects($this->any())->willReturn([]);
|
||||
|
||||
$gauge = $this->collector->register_gauge('test_render_metric', 'Test metric for render');
|
||||
$gauge->set(42, []);
|
||||
|
||||
$output = $this->collector->render();
|
||||
|
||||
$this->assertStringContainsString('wordpress_test_render_metric', $output);
|
||||
$this->assertStringContainsString('42', $output);
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
|
||||
private function resetRuntimeCollectorSingleton(): void
|
||||
{
|
||||
$reflection = new \ReflectionClass(RuntimeCollector::class);
|
||||
$property = $reflection->getProperty('instance');
|
||||
$property->setValue(null, null);
|
||||
}
|
||||
|
||||
private function resetStorageFactory(): void
|
||||
{
|
||||
$reflection = new \ReflectionClass(StorageFactory::class);
|
||||
$property = $reflection->getProperty('instance');
|
||||
$property->setValue(null, null);
|
||||
}
|
||||
}
|
||||
655
tests/Unit/Metrics/CustomMetricBuilderTest.php
Normal file
655
tests/Unit/Metrics/CustomMetricBuilderTest.php
Normal file
@@ -0,0 +1,655 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WpPrometheus\Tests\Unit\Metrics;
|
||||
|
||||
use Magdev\WpPrometheus\Metrics\CustomMetricBuilder;
|
||||
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
|
||||
#[CoversClass(CustomMetricBuilder::class)]
|
||||
class CustomMetricBuilderTest extends TestCase
|
||||
{
|
||||
private CustomMetricBuilder $builder;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->builder = new CustomMetricBuilder();
|
||||
}
|
||||
|
||||
// ── validate_name() ──────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('validMetricNamesProvider')]
|
||||
public function validate_name_accepts_valid_names(string $name): void
|
||||
{
|
||||
$this->assertTrue($this->builder->validate_name($name));
|
||||
}
|
||||
|
||||
public static function validMetricNamesProvider(): array
|
||||
{
|
||||
return [
|
||||
'simple' => ['my_metric'],
|
||||
'with_colon' => ['my:metric'],
|
||||
'starts_with_underscore' => ['_private_metric'],
|
||||
'starts_with_colon' => [':special_metric'],
|
||||
'uppercase' => ['MY_METRIC'],
|
||||
'mixed_case' => ['myMetric123'],
|
||||
'single_letter' => ['m'],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidMetricNamesProvider')]
|
||||
public function validate_name_rejects_invalid_names(string $name): void
|
||||
{
|
||||
$this->assertFalse($this->builder->validate_name($name));
|
||||
}
|
||||
|
||||
public static function invalidMetricNamesProvider(): array
|
||||
{
|
||||
return [
|
||||
'starts_with_digit' => ['0metric'],
|
||||
'contains_dash' => ['my-metric'],
|
||||
'contains_space' => ['my metric'],
|
||||
'contains_dot' => ['my.metric'],
|
||||
'empty_string' => [''],
|
||||
'special_chars' => ['metric@name'],
|
||||
];
|
||||
}
|
||||
|
||||
// ── validate_label_name() ────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('validLabelNamesProvider')]
|
||||
public function validate_label_name_accepts_valid_names(string $name): void
|
||||
{
|
||||
$this->assertTrue($this->builder->validate_label_name($name));
|
||||
}
|
||||
|
||||
public static function validLabelNamesProvider(): array
|
||||
{
|
||||
return [
|
||||
'simple' => ['status'],
|
||||
'with_underscore' => ['http_method'],
|
||||
'starts_with_underscore' => ['_internal'],
|
||||
'uppercase' => ['METHOD'],
|
||||
'alphanumeric' => ['label1'],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidLabelNamesProvider')]
|
||||
public function validate_label_name_rejects_invalid_names(string $name): void
|
||||
{
|
||||
$this->assertFalse($this->builder->validate_label_name($name));
|
||||
}
|
||||
|
||||
public static function invalidLabelNamesProvider(): array
|
||||
{
|
||||
return [
|
||||
'double_underscore_prefix' => ['__reserved'],
|
||||
'starts_with_digit' => ['1label'],
|
||||
'contains_colon' => ['label:name'],
|
||||
'contains_dash' => ['label-name'],
|
||||
'empty_string' => [''],
|
||||
'contains_space' => ['label name'],
|
||||
];
|
||||
}
|
||||
|
||||
// ── validate() ───────────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function validate_returns_empty_for_valid_metric(): void
|
||||
{
|
||||
$errors = $this->builder->validate($this->validMetric());
|
||||
$this->assertEmpty($errors);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_requires_name(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['name'] = '';
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('name is required', $errors[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_rejects_invalid_metric_name(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['name'] = '0invalid';
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('must start with', $errors[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('reservedPrefixProvider')]
|
||||
public function validate_rejects_reserved_prefix(string $prefix): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['name'] = $prefix . 'test';
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('reserved prefix', implode(' ', $errors));
|
||||
}
|
||||
|
||||
public static function reservedPrefixProvider(): array
|
||||
{
|
||||
return [
|
||||
'wordpress_' => ['wordpress_'],
|
||||
'go_' => ['go_'],
|
||||
'process_' => ['process_'],
|
||||
'promhttp_' => ['promhttp_'],
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_detects_duplicate_name(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||
'existing-id' => [
|
||||
'id' => 'existing-id',
|
||||
'name' => 'custom_existing',
|
||||
'help' => 'test',
|
||||
],
|
||||
];
|
||||
|
||||
$metric = $this->validMetric();
|
||||
$metric['name'] = 'custom_existing';
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('already exists', implode(' ', $errors));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_allows_same_name_when_editing(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||
'my-id' => [
|
||||
'id' => 'my-id',
|
||||
'name' => 'custom_existing',
|
||||
'help' => 'test',
|
||||
],
|
||||
];
|
||||
|
||||
$metric = $this->validMetric();
|
||||
$metric['id'] = 'my-id';
|
||||
$metric['name'] = 'custom_existing';
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertEmpty($errors);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_requires_help_text(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['help'] = '';
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('Help text is required', $errors[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_requires_valid_type(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['type'] = 'counter';
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('Invalid metric type', implode(' ', $errors));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_rejects_too_many_labels(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['labels'] = ['a', 'b', 'c', 'd', 'e', 'f'];
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('Maximum', implode(' ', $errors));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_rejects_invalid_label_names_in_array(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['labels'] = ['valid', '__reserved'];
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('Invalid label name', implode(' ', $errors));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_rejects_non_array_labels(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['labels'] = 'not_an_array';
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('Labels must be an array', implode(' ', $errors));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_requires_valid_value_type(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['value_type'] = 'invalid';
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('Invalid value type', implode(' ', $errors));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_requires_option_name_for_option_type(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['value_type'] = 'option';
|
||||
$metric['value_config'] = [];
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('Option name is required', implode(' ', $errors));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_accepts_option_type_with_option_name(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['value_type'] = 'option';
|
||||
$metric['value_config'] = ['option_name' => 'my_wp_option'];
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertEmpty($errors);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_rejects_too_many_label_values(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['labels'] = ['status'];
|
||||
$metric['label_values'] = array_fill(0, 51, ['active', 1.0]);
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('Maximum', implode(' ', $errors));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validate_checks_label_value_row_count(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['labels'] = ['status', 'type'];
|
||||
// Row has 2 items but needs 3 (2 labels + 1 value).
|
||||
$metric['label_values'] = [['active', 1.0]];
|
||||
|
||||
$errors = $this->builder->validate($metric);
|
||||
$this->assertNotEmpty($errors);
|
||||
$this->assertStringContainsString('values for all labels', implode(' ', $errors));
|
||||
}
|
||||
|
||||
// ── get_all() / get() ────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function get_all_returns_empty_array_by_default(): void
|
||||
{
|
||||
$this->assertSame([], $this->builder->get_all());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_all_returns_stored_metrics(): void
|
||||
{
|
||||
$metrics = ['id1' => ['name' => 'test_metric']];
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = $metrics;
|
||||
|
||||
$this->assertSame($metrics, $this->builder->get_all());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_all_returns_empty_when_option_is_not_array(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = 'not_an_array';
|
||||
$this->assertSame([], $this->builder->get_all());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_returns_metric_by_id(): void
|
||||
{
|
||||
$metric = ['id' => 'my-id', 'name' => 'test_metric', 'help' => 'Test'];
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = ['my-id' => $metric];
|
||||
|
||||
$this->assertSame($metric, $this->builder->get('my-id'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_returns_null_for_nonexistent_id(): void
|
||||
{
|
||||
$this->assertNull($this->builder->get('nonexistent'));
|
||||
}
|
||||
|
||||
// ── save() ───────────────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function save_creates_new_metric_and_returns_id(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$id = $this->builder->save($metric);
|
||||
|
||||
$this->assertNotEmpty($id);
|
||||
$this->assertGreaterThanOrEqual(1, GlobalFunctionState::getCallCount('update_option'));
|
||||
|
||||
$saved = GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME];
|
||||
$this->assertArrayHasKey($id, $saved);
|
||||
$this->assertSame('custom_test_metric', $saved[$id]['name']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function save_throws_on_validation_failure(): void
|
||||
{
|
||||
$metric = [
|
||||
'name' => '',
|
||||
'help' => '',
|
||||
'type' => 'gauge',
|
||||
'value_type' => 'static',
|
||||
];
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->builder->save($metric);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function save_updates_existing_metric(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||
'existing-id' => [
|
||||
'id' => 'existing-id',
|
||||
'name' => 'custom_test_metric',
|
||||
'help' => 'Original help',
|
||||
'type' => 'gauge',
|
||||
'value_type' => 'static',
|
||||
'labels' => [],
|
||||
'created_at' => 1000000,
|
||||
],
|
||||
];
|
||||
|
||||
$metric = $this->validMetric();
|
||||
$metric['id'] = 'existing-id';
|
||||
$metric['help'] = 'Updated help';
|
||||
|
||||
$id = $this->builder->save($metric);
|
||||
|
||||
$this->assertSame('existing-id', $id);
|
||||
$saved = GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME];
|
||||
$this->assertSame('Updated help', $saved['existing-id']['help']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function save_sanitizes_metric_data(): void
|
||||
{
|
||||
$metric = $this->validMetric();
|
||||
$metric['labels'] = ['valid_label'];
|
||||
$metric['label_values'] = [['active', 42.5]];
|
||||
|
||||
$id = $this->builder->save($metric);
|
||||
$saved = GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME][$id];
|
||||
|
||||
$this->assertSame(['valid_label'], $saved['labels']);
|
||||
$this->assertSame([['active', 42.5]], $saved['label_values']);
|
||||
$this->assertIsBool($saved['enabled']);
|
||||
}
|
||||
|
||||
// ── delete() ─────────────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function delete_removes_metric(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||
'my-id' => ['id' => 'my-id', 'name' => 'custom_metric'],
|
||||
];
|
||||
|
||||
$this->assertTrue($this->builder->delete('my-id'));
|
||||
$this->assertArrayNotHasKey(
|
||||
'my-id',
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME]
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function delete_returns_false_for_nonexistent(): void
|
||||
{
|
||||
$this->assertFalse($this->builder->delete('nonexistent'));
|
||||
}
|
||||
|
||||
// ── export() ─────────────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function export_returns_valid_json_with_metadata(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||
'id1' => ['name' => 'metric1', 'help' => 'Test 1'],
|
||||
];
|
||||
|
||||
$json = $this->builder->export();
|
||||
$data = json_decode($json, true);
|
||||
|
||||
$this->assertIsArray($data);
|
||||
$this->assertSame(CustomMetricBuilder::EXPORT_VERSION, $data['version']);
|
||||
$this->assertSame(WP_PROMETHEUS_VERSION, $data['plugin_version']);
|
||||
$this->assertArrayHasKey('exported_at', $data);
|
||||
$this->assertCount(1, $data['metrics']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function export_does_not_include_site_url(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [];
|
||||
|
||||
$json = $this->builder->export();
|
||||
$data = json_decode($json, true);
|
||||
|
||||
$this->assertArrayNotHasKey('site_url', $data);
|
||||
}
|
||||
|
||||
// ── import() ─────────────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function import_rejects_oversized_json(): void
|
||||
{
|
||||
$json = str_repeat('x', CustomMetricBuilder::MAX_IMPORT_SIZE + 1);
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('exceeds maximum size');
|
||||
$this->builder->import($json);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function import_rejects_invalid_json(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Invalid JSON');
|
||||
$this->builder->import('{invalid json');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function import_rejects_missing_metrics_key(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('No metrics found');
|
||||
$this->builder->import('{"version":"1.0.0"}');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function import_skip_mode_skips_duplicates(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||
'existing' => [
|
||||
'id' => 'existing',
|
||||
'name' => 'custom_existing_metric',
|
||||
'help' => 'Test',
|
||||
],
|
||||
];
|
||||
|
||||
$json = json_encode([
|
||||
'version' => '1.0.0',
|
||||
'metrics' => [
|
||||
[
|
||||
'name' => 'custom_existing_metric',
|
||||
'help' => 'Test',
|
||||
'type' => 'gauge',
|
||||
'value_type' => 'static',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$result = $this->builder->import($json, 'skip');
|
||||
$this->assertSame(1, $result['skipped']);
|
||||
$this->assertSame(0, $result['imported']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function import_rename_mode_renames_duplicates(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||
'existing' => [
|
||||
'id' => 'existing',
|
||||
'name' => 'custom_existing_metric',
|
||||
'help' => 'Existing',
|
||||
'type' => 'gauge',
|
||||
'value_type' => 'static',
|
||||
'labels' => [],
|
||||
],
|
||||
];
|
||||
|
||||
$json = json_encode([
|
||||
'version' => '1.0.0',
|
||||
'metrics' => [
|
||||
[
|
||||
'name' => 'custom_existing_metric',
|
||||
'help' => 'Imported',
|
||||
'type' => 'gauge',
|
||||
'value_type' => 'static',
|
||||
'labels' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$result = $this->builder->import($json, 'rename');
|
||||
$this->assertSame(1, $result['imported']);
|
||||
|
||||
$all = $this->builder->get_all();
|
||||
$names = array_column($all, 'name');
|
||||
$this->assertContains('custom_existing_metric', $names);
|
||||
$this->assertContains('custom_existing_metric_imported_1', $names);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function import_invalid_mode_defaults_to_skip(): void
|
||||
{
|
||||
GlobalFunctionState::$options[CustomMetricBuilder::OPTION_NAME] = [
|
||||
'existing' => [
|
||||
'id' => 'existing',
|
||||
'name' => 'custom_existing_metric',
|
||||
'help' => 'Test',
|
||||
],
|
||||
];
|
||||
|
||||
$json = json_encode([
|
||||
'version' => '1.0.0',
|
||||
'metrics' => [
|
||||
[
|
||||
'name' => 'custom_existing_metric',
|
||||
'help' => 'Test',
|
||||
'type' => 'gauge',
|
||||
'value_type' => 'static',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$result = $this->builder->import($json, 'invalid_mode');
|
||||
$this->assertSame(1, $result['skipped']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function import_counts_metrics_without_name_as_errors(): void
|
||||
{
|
||||
$json = json_encode([
|
||||
'version' => '1.0.0',
|
||||
'metrics' => [['help' => 'No name']],
|
||||
]);
|
||||
|
||||
$result = $this->builder->import($json);
|
||||
$this->assertSame(1, $result['errors']);
|
||||
$this->assertSame(0, $result['imported']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function import_successfully_imports_new_metric(): void
|
||||
{
|
||||
$json = json_encode([
|
||||
'version' => '1.0.0',
|
||||
'metrics' => [
|
||||
[
|
||||
'name' => 'custom_new_metric',
|
||||
'help' => 'A new metric',
|
||||
'type' => 'gauge',
|
||||
'value_type' => 'static',
|
||||
'labels' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$result = $this->builder->import($json);
|
||||
$this->assertSame(1, $result['imported']);
|
||||
$this->assertSame(0, $result['skipped']);
|
||||
$this->assertSame(0, $result['errors']);
|
||||
|
||||
$all = $this->builder->get_all();
|
||||
$names = array_column($all, 'name');
|
||||
$this->assertContains('custom_new_metric', $names);
|
||||
}
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function constants_are_defined(): void
|
||||
{
|
||||
$this->assertSame('wp_prometheus_custom_metrics', CustomMetricBuilder::OPTION_NAME);
|
||||
$this->assertSame(5, CustomMetricBuilder::MAX_LABELS);
|
||||
$this->assertSame(50, CustomMetricBuilder::MAX_LABEL_VALUES);
|
||||
$this->assertSame(1048576, CustomMetricBuilder::MAX_IMPORT_SIZE);
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
private function validMetric(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'custom_test_metric',
|
||||
'help' => 'A test metric',
|
||||
'type' => 'gauge',
|
||||
'value_type' => 'static',
|
||||
'labels' => [],
|
||||
'label_values' => [],
|
||||
'enabled' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
234
tests/Unit/Metrics/RuntimeCollectorTest.php
Normal file
234
tests/Unit/Metrics/RuntimeCollectorTest.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WpPrometheus\Tests\Unit\Metrics;
|
||||
|
||||
use Magdev\WpPrometheus\Metrics\RuntimeCollector;
|
||||
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
|
||||
#[CoversClass(RuntimeCollector::class)]
|
||||
class RuntimeCollectorTest extends TestCase
|
||||
{
|
||||
private RuntimeCollector $collector;
|
||||
private array $originalServer = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->originalServer = $_SERVER;
|
||||
$this->collector = $this->createInstance();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER = $this->originalServer;
|
||||
$this->resetSingleton();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
// ── Singleton ────────────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function get_instance_returns_singleton(): void
|
||||
{
|
||||
$instance1 = RuntimeCollector::get_instance();
|
||||
$instance2 = RuntimeCollector::get_instance();
|
||||
|
||||
$this->assertSame($instance1, $instance2);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_instance_returns_runtime_collector(): void
|
||||
{
|
||||
$instance = RuntimeCollector::get_instance();
|
||||
$this->assertInstanceOf(RuntimeCollector::class, $instance);
|
||||
}
|
||||
|
||||
// ── get_stored_metrics() ─────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function get_stored_metrics_returns_default_when_empty(): void
|
||||
{
|
||||
$metrics = $this->collector->get_stored_metrics();
|
||||
$this->assertSame([], $metrics);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_stored_metrics_returns_stored_data(): void
|
||||
{
|
||||
$stored = [
|
||||
'counters' => ['key' => ['name' => 'test', 'value' => 5]],
|
||||
'histograms' => [],
|
||||
'last_reset' => 1000000,
|
||||
];
|
||||
GlobalFunctionState::$options['wp_prometheus_runtime_metrics'] = $stored;
|
||||
|
||||
$metrics = $this->collector->get_stored_metrics();
|
||||
$this->assertSame($stored, $metrics);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_stored_metrics_returns_default_for_non_array(): void
|
||||
{
|
||||
GlobalFunctionState::$options['wp_prometheus_runtime_metrics'] = 'invalid';
|
||||
|
||||
$metrics = $this->collector->get_stored_metrics();
|
||||
$this->assertArrayHasKey('counters', $metrics);
|
||||
$this->assertArrayHasKey('histograms', $metrics);
|
||||
$this->assertArrayHasKey('last_reset', $metrics);
|
||||
}
|
||||
|
||||
// ── reset_metrics() ──────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function reset_metrics_deletes_option(): void
|
||||
{
|
||||
GlobalFunctionState::$options['wp_prometheus_runtime_metrics'] = ['data'];
|
||||
|
||||
RuntimeCollector::reset_metrics();
|
||||
|
||||
$this->assertSame(1, GlobalFunctionState::getCallCount('delete_option'));
|
||||
$this->assertArrayNotHasKey(
|
||||
'wp_prometheus_runtime_metrics',
|
||||
GlobalFunctionState::$options
|
||||
);
|
||||
}
|
||||
|
||||
// ── get_duration_buckets() ───────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function get_duration_buckets_returns_expected_values(): void
|
||||
{
|
||||
$buckets = RuntimeCollector::get_duration_buckets();
|
||||
|
||||
$this->assertIsArray($buckets);
|
||||
$this->assertCount(11, $buckets);
|
||||
$this->assertSame(0.005, $buckets[0]);
|
||||
$this->assertEquals(10, end($buckets));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function duration_buckets_are_in_ascending_order(): void
|
||||
{
|
||||
$buckets = RuntimeCollector::get_duration_buckets();
|
||||
$sorted = $buckets;
|
||||
sort($sorted);
|
||||
|
||||
$this->assertSame($sorted, $buckets);
|
||||
}
|
||||
|
||||
// ── get_normalized_endpoint() (private, via reflection) ──────────
|
||||
|
||||
#[Test]
|
||||
public function normalized_endpoint_returns_admin_when_is_admin(): void
|
||||
{
|
||||
$_SERVER['REQUEST_URI'] = '/wp-admin/options-general.php';
|
||||
GlobalFunctionState::$options['__is_admin'] = true;
|
||||
|
||||
$result = $this->callPrivateMethod('get_normalized_endpoint');
|
||||
$this->assertSame('admin', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function normalized_endpoint_returns_ajax_when_doing_ajax(): void
|
||||
{
|
||||
$_SERVER['REQUEST_URI'] = '/wp-admin/admin-ajax.php';
|
||||
GlobalFunctionState::$options['__wp_doing_ajax'] = true;
|
||||
|
||||
$result = $this->callPrivateMethod('get_normalized_endpoint');
|
||||
$this->assertSame('ajax', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function normalized_endpoint_returns_cron_when_doing_cron(): void
|
||||
{
|
||||
// Use a generic URI (not /wp-cron.php) to ensure function is checked, not URL pattern.
|
||||
$_SERVER['REQUEST_URI'] = '/some-page';
|
||||
GlobalFunctionState::$options['__wp_doing_cron'] = true;
|
||||
|
||||
$result = $this->callPrivateMethod('get_normalized_endpoint');
|
||||
$this->assertSame('cron', $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('urlEndpointProvider')]
|
||||
public function normalized_endpoint_from_url_pattern(string $uri, string $expected): void
|
||||
{
|
||||
$_SERVER['REQUEST_URI'] = $uri;
|
||||
|
||||
$result = $this->callPrivateMethod('get_normalized_endpoint');
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public static function urlEndpointProvider(): array
|
||||
{
|
||||
return [
|
||||
'rest api' => ['/wp-json/wp/v2/posts', 'rest-api'],
|
||||
'login page' => ['/wp-login.php', 'login'],
|
||||
'login with query' => ['/wp-login.php?action=login', 'login'],
|
||||
'wp-cron' => ['/wp-cron.php', 'cron'],
|
||||
'feed root' => ['/feed/', 'feed'],
|
||||
'feed trailing' => ['/category/news/feed', 'feed'],
|
||||
'feed with slash' => ['/feed', 'feed'],
|
||||
'homepage' => ['/', 'frontend'],
|
||||
'page' => ['/about-us', 'frontend'],
|
||||
'post' => ['/2024/01/hello-world', 'frontend'],
|
||||
];
|
||||
}
|
||||
|
||||
// ── is_metrics_request() (private, via reflection) ───────────────
|
||||
|
||||
#[Test]
|
||||
public function is_metrics_request_true_for_metrics_uri(): void
|
||||
{
|
||||
$_SERVER['REQUEST_URI'] = '/metrics';
|
||||
$this->assertTrue($this->callPrivateMethod('is_metrics_request'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function is_metrics_request_true_with_trailing_slash(): void
|
||||
{
|
||||
$_SERVER['REQUEST_URI'] = '/metrics/';
|
||||
$this->assertTrue($this->callPrivateMethod('is_metrics_request'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function is_metrics_request_false_for_other_uri(): void
|
||||
{
|
||||
$_SERVER['REQUEST_URI'] = '/some-page';
|
||||
$this->assertFalse($this->callPrivateMethod('is_metrics_request'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function is_metrics_request_false_when_no_uri(): void
|
||||
{
|
||||
unset($_SERVER['REQUEST_URI']);
|
||||
$this->assertFalse($this->callPrivateMethod('is_metrics_request'));
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
private function createInstance(): RuntimeCollector
|
||||
{
|
||||
$reflection = new \ReflectionClass(RuntimeCollector::class);
|
||||
return $reflection->newInstanceWithoutConstructor();
|
||||
}
|
||||
|
||||
private function resetSingleton(): void
|
||||
{
|
||||
$reflection = new \ReflectionClass(RuntimeCollector::class);
|
||||
$property = $reflection->getProperty('instance');
|
||||
$property->setValue(null, null);
|
||||
}
|
||||
|
||||
private function callPrivateMethod(string $method, array $args = []): mixed
|
||||
{
|
||||
$reflection = new \ReflectionMethod($this->collector, $method);
|
||||
return $reflection->invoke($this->collector, ...$args);
|
||||
}
|
||||
}
|
||||
411
tests/Unit/Metrics/StorageFactoryTest.php
Normal file
411
tests/Unit/Metrics/StorageFactoryTest.php
Normal file
@@ -0,0 +1,411 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Magdev\WpPrometheus\Tests\Unit\Metrics;
|
||||
|
||||
use Magdev\WpPrometheus\Metrics\StorageFactory;
|
||||
use Magdev\WpPrometheus\Tests\Helpers\GlobalFunctionState;
|
||||
use Magdev\WpPrometheus\Tests\Unit\TestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Prometheus\Storage\InMemory;
|
||||
|
||||
#[CoversClass(StorageFactory::class)]
|
||||
class StorageFactoryTest extends TestCase
|
||||
{
|
||||
/** @var list<string> Environment variables to clean up after each test. */
|
||||
private array $envVarsToClean = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
StorageFactory::reset();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
StorageFactory::reset();
|
||||
foreach ($this->envVarsToClean as $var) {
|
||||
putenv($var);
|
||||
}
|
||||
$this->envVarsToClean = [];
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
// ── Adapter Constants ────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function adapter_constants_are_defined(): void
|
||||
{
|
||||
$this->assertSame('inmemory', StorageFactory::ADAPTER_INMEMORY);
|
||||
$this->assertSame('redis', StorageFactory::ADAPTER_REDIS);
|
||||
$this->assertSame('apcu', StorageFactory::ADAPTER_APCU);
|
||||
}
|
||||
|
||||
// ── get_available_adapters() ─────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function get_available_adapters_returns_all_three(): void
|
||||
{
|
||||
$adapters = StorageFactory::get_available_adapters();
|
||||
|
||||
$this->assertArrayHasKey(StorageFactory::ADAPTER_INMEMORY, $adapters);
|
||||
$this->assertArrayHasKey(StorageFactory::ADAPTER_REDIS, $adapters);
|
||||
$this->assertArrayHasKey(StorageFactory::ADAPTER_APCU, $adapters);
|
||||
$this->assertCount(3, $adapters);
|
||||
}
|
||||
|
||||
// ── is_adapter_available() ───────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function inmemory_adapter_is_always_available(): void
|
||||
{
|
||||
$this->assertTrue(StorageFactory::is_adapter_available(StorageFactory::ADAPTER_INMEMORY));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function unknown_adapter_is_not_available(): void
|
||||
{
|
||||
$this->assertFalse(StorageFactory::is_adapter_available('unknown'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function redis_availability_depends_on_extension(): void
|
||||
{
|
||||
$extensionLoaded = $this->getFunctionMock(
|
||||
'Magdev\\WpPrometheus\\Metrics',
|
||||
'extension_loaded'
|
||||
);
|
||||
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||
|
||||
$this->assertFalse(StorageFactory::is_adapter_available(StorageFactory::ADAPTER_REDIS));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function apcu_availability_requires_extension_and_enabled(): void
|
||||
{
|
||||
$extensionLoaded = $this->getFunctionMock(
|
||||
'Magdev\\WpPrometheus\\Metrics',
|
||||
'extension_loaded'
|
||||
);
|
||||
$extensionLoaded->expects($this->any())->willReturn(true);
|
||||
|
||||
$apcuEnabled = $this->getFunctionMock(
|
||||
'Magdev\\WpPrometheus\\Metrics',
|
||||
'apcu_enabled'
|
||||
);
|
||||
$apcuEnabled->expects($this->any())->willReturn(false);
|
||||
|
||||
$this->assertFalse(StorageFactory::is_adapter_available(StorageFactory::ADAPTER_APCU));
|
||||
}
|
||||
|
||||
// ── get_configured_adapter() ─────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function default_configured_adapter_is_inmemory(): void
|
||||
{
|
||||
$this->assertSame('inmemory', StorageFactory::get_configured_adapter());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configured_adapter_reads_from_env_var(): void
|
||||
{
|
||||
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'redis');
|
||||
|
||||
$this->assertSame('redis', StorageFactory::get_configured_adapter());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configured_adapter_reads_from_option(): void
|
||||
{
|
||||
GlobalFunctionState::$options['wp_prometheus_storage_adapter'] = 'apcu';
|
||||
|
||||
$this->assertSame('apcu', StorageFactory::get_configured_adapter());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function env_var_takes_precedence_over_option(): void
|
||||
{
|
||||
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'redis');
|
||||
GlobalFunctionState::$options['wp_prometheus_storage_adapter'] = 'apcu';
|
||||
|
||||
$this->assertSame('redis', StorageFactory::get_configured_adapter());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function configured_adapter_lowercases_env_value(): void
|
||||
{
|
||||
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'REDIS');
|
||||
|
||||
$this->assertSame('redis', StorageFactory::get_configured_adapter());
|
||||
}
|
||||
|
||||
// ── get_redis_config() ───────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function get_redis_config_returns_defaults(): void
|
||||
{
|
||||
$config = StorageFactory::get_redis_config();
|
||||
|
||||
$this->assertSame('127.0.0.1', $config['host']);
|
||||
$this->assertSame(6379, $config['port']);
|
||||
$this->assertSame('', $config['password']);
|
||||
$this->assertSame(0, $config['database']);
|
||||
$this->assertSame('WORDPRESS_PROMETHEUS_', $config['prefix']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_redis_config_reads_from_env_vars(): void
|
||||
{
|
||||
$this->setEnv('WP_PROMETHEUS_REDIS_HOST', '10.0.0.1');
|
||||
$this->setEnv('WP_PROMETHEUS_REDIS_PORT', '6380');
|
||||
$this->setEnv('WP_PROMETHEUS_REDIS_PASSWORD', 's3cret');
|
||||
$this->setEnv('WP_PROMETHEUS_REDIS_DATABASE', '2');
|
||||
$this->setEnv('WP_PROMETHEUS_REDIS_PREFIX', 'MY_PREFIX_');
|
||||
|
||||
$config = StorageFactory::get_redis_config();
|
||||
|
||||
$this->assertSame('10.0.0.1', $config['host']);
|
||||
$this->assertSame(6380, $config['port']);
|
||||
$this->assertSame('s3cret', $config['password']);
|
||||
$this->assertSame(2, $config['database']);
|
||||
$this->assertSame('MY_PREFIX_', $config['prefix']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_redis_config_reads_from_option(): void
|
||||
{
|
||||
GlobalFunctionState::$options['wp_prometheus_redis_config'] = [
|
||||
'host' => '192.168.1.1',
|
||||
'port' => 6381,
|
||||
'password' => 'optpass',
|
||||
'database' => 3,
|
||||
'prefix' => 'OPT_',
|
||||
];
|
||||
|
||||
$config = StorageFactory::get_redis_config();
|
||||
|
||||
$this->assertSame('192.168.1.1', $config['host']);
|
||||
$this->assertSame(6381, $config['port']);
|
||||
$this->assertSame('optpass', $config['password']);
|
||||
$this->assertSame(3, $config['database']);
|
||||
$this->assertSame('OPT_', $config['prefix']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_redis_config_env_takes_precedence(): void
|
||||
{
|
||||
$this->setEnv('WP_PROMETHEUS_REDIS_HOST', 'env-host');
|
||||
GlobalFunctionState::$options['wp_prometheus_redis_config'] = [
|
||||
'host' => 'option-host',
|
||||
];
|
||||
|
||||
$config = StorageFactory::get_redis_config();
|
||||
$this->assertSame('env-host', $config['host']);
|
||||
}
|
||||
|
||||
// ── get_apcu_prefix() ────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function get_apcu_prefix_returns_default(): void
|
||||
{
|
||||
$this->assertSame('wp_prom', StorageFactory::get_apcu_prefix());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_apcu_prefix_reads_from_env(): void
|
||||
{
|
||||
$this->setEnv('WP_PROMETHEUS_APCU_PREFIX', 'custom_prefix');
|
||||
|
||||
$this->assertSame('custom_prefix', StorageFactory::get_apcu_prefix());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_apcu_prefix_reads_from_option(): void
|
||||
{
|
||||
GlobalFunctionState::$options['wp_prometheus_apcu_prefix'] = 'opt_prefix';
|
||||
|
||||
$this->assertSame('opt_prefix', StorageFactory::get_apcu_prefix());
|
||||
}
|
||||
|
||||
// ── save_config() ────────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function save_config_stores_adapter(): void
|
||||
{
|
||||
StorageFactory::save_config(['adapter' => 'redis']);
|
||||
|
||||
$this->assertSame(
|
||||
'redis',
|
||||
GlobalFunctionState::$options['wp_prometheus_storage_adapter']
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function save_config_stores_redis_config(): void
|
||||
{
|
||||
StorageFactory::save_config([
|
||||
'redis' => [
|
||||
'host' => '10.0.0.5',
|
||||
'port' => 6390,
|
||||
'password' => 'pass123',
|
||||
'database' => 1,
|
||||
'prefix' => 'TEST_',
|
||||
],
|
||||
]);
|
||||
|
||||
$saved = GlobalFunctionState::$options['wp_prometheus_redis_config'];
|
||||
$this->assertSame('10.0.0.5', $saved['host']);
|
||||
$this->assertSame(6390, $saved['port']);
|
||||
$this->assertSame('pass123', $saved['password']);
|
||||
$this->assertSame(1, $saved['database']);
|
||||
// sanitize_key lowercases the prefix
|
||||
$this->assertSame('test_', $saved['prefix']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function save_config_stores_apcu_prefix(): void
|
||||
{
|
||||
StorageFactory::save_config(['apcu_prefix' => 'my_apcu']);
|
||||
|
||||
$this->assertSame(
|
||||
'my_apcu',
|
||||
GlobalFunctionState::$options['wp_prometheus_apcu_prefix']
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function save_config_resets_singleton(): void
|
||||
{
|
||||
// Get an adapter (creates singleton).
|
||||
$adapter1 = StorageFactory::get_adapter();
|
||||
|
||||
// Save new config (should reset singleton).
|
||||
StorageFactory::save_config(['adapter' => 'inmemory']);
|
||||
|
||||
// Get adapter again — should be a new instance.
|
||||
$adapter2 = StorageFactory::get_adapter();
|
||||
$this->assertNotSame($adapter1, $adapter2);
|
||||
}
|
||||
|
||||
// ── test_connection() ────────────────────────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function test_connection_inmemory_always_succeeds(): void
|
||||
{
|
||||
$result = StorageFactory::test_connection(StorageFactory::ADAPTER_INMEMORY);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertNotEmpty($result['message']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function test_connection_unknown_adapter_fails(): void
|
||||
{
|
||||
$result = StorageFactory::test_connection('unknown');
|
||||
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Unknown', $result['message']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function test_connection_redis_fails_without_extension(): void
|
||||
{
|
||||
$extensionLoaded = $this->getFunctionMock(
|
||||
'Magdev\\WpPrometheus\\Metrics',
|
||||
'extension_loaded'
|
||||
);
|
||||
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||
|
||||
$result = StorageFactory::test_connection(StorageFactory::ADAPTER_REDIS);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('not installed', $result['message']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function test_connection_apcu_fails_without_extension(): void
|
||||
{
|
||||
$extensionLoaded = $this->getFunctionMock(
|
||||
'Magdev\\WpPrometheus\\Metrics',
|
||||
'extension_loaded'
|
||||
);
|
||||
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||
|
||||
$result = StorageFactory::test_connection(StorageFactory::ADAPTER_APCU);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('not installed', $result['message']);
|
||||
}
|
||||
|
||||
// ── get_adapter() / reset() / singleton ──────────────────────────
|
||||
|
||||
#[Test]
|
||||
public function get_adapter_returns_inmemory_by_default(): void
|
||||
{
|
||||
$adapter = StorageFactory::get_adapter();
|
||||
$this->assertInstanceOf(InMemory::class, $adapter);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_adapter_returns_singleton(): void
|
||||
{
|
||||
$adapter1 = StorageFactory::get_adapter();
|
||||
$adapter2 = StorageFactory::get_adapter();
|
||||
|
||||
$this->assertSame($adapter1, $adapter2);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reset_clears_singleton_and_error(): void
|
||||
{
|
||||
StorageFactory::get_adapter();
|
||||
StorageFactory::reset();
|
||||
|
||||
// After reset, get_last_error should be empty.
|
||||
$this->assertEmpty(StorageFactory::get_last_error());
|
||||
|
||||
// Getting adapter again creates a new instance.
|
||||
$adapter = StorageFactory::get_adapter();
|
||||
$this->assertInstanceOf(InMemory::class, $adapter);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_adapter_falls_back_to_inmemory_when_redis_unavailable(): void
|
||||
{
|
||||
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'redis');
|
||||
|
||||
$extensionLoaded = $this->getFunctionMock(
|
||||
'Magdev\\WpPrometheus\\Metrics',
|
||||
'extension_loaded'
|
||||
);
|
||||
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||
|
||||
$adapter = StorageFactory::get_adapter();
|
||||
$this->assertInstanceOf(InMemory::class, $adapter);
|
||||
$this->assertNotEmpty(StorageFactory::get_last_error());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function get_adapter_falls_back_to_inmemory_when_apcu_unavailable(): void
|
||||
{
|
||||
$this->setEnv('WP_PROMETHEUS_STORAGE_ADAPTER', 'apcu');
|
||||
|
||||
$extensionLoaded = $this->getFunctionMock(
|
||||
'Magdev\\WpPrometheus\\Metrics',
|
||||
'extension_loaded'
|
||||
);
|
||||
$extensionLoaded->expects($this->any())->willReturn(false);
|
||||
|
||||
$adapter = StorageFactory::get_adapter();
|
||||
$this->assertInstanceOf(InMemory::class, $adapter);
|
||||
$this->assertNotEmpty(StorageFactory::get_last_error());
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
private function setEnv(string $name, string $value): void
|
||||
{
|
||||
putenv("$name=$value");
|
||||
$this->envVarsToClean[] = $name;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user