You've already forked wp-prometheus
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>
656 lines
22 KiB
PHP
656 lines
22 KiB
PHP
<?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,
|
|
];
|
|
}
|
|
}
|