diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 244fb8b..92987bc 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -46,6 +46,30 @@ define( 'PRV_TARGET_DOMAIN', 'peptiderepo.com' ); define( 'PRV_IO_RETENTION_DEFAULT_DAYS', 90 ); define( 'PRV_DAILY_PRUNE_HOOK', 'prv_daily_prune' ); +// Salt constants required by PRV_Key_Store::derive_encryption_key(). +// These provide deterministic 64-char salts matching the pattern in test-key-store.php. +if ( ! defined( 'AUTH_KEY' ) ) { + define( 'AUTH_KEY', 'test-auth-key-at-least-64-chars-long-for-realistic-entropy-aaaa' ); +} +if ( ! defined( 'SECURE_AUTH_KEY' ) ) { + define( 'SECURE_AUTH_KEY', 'test-secure-auth-key-64-chars-long-realistic-entropy-bbbb-cccc' ); +} + +// Provider key constant; defined here once so no test class defines it mid-run. +// Tests that need a configured key get it from this constant (via PRV_Key_Store::get_key()). +// The sentinel leak test (CaptureWriterTest) asserts this value does not appear in stored rows. +if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) { + define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' ); +} + +// Cloudflare gateway constants (empty in test environment). +if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) { + define( 'PRV_CF_ACCOUNT_ID', '' ); +} +if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) { + define( 'PRV_CF_GATEWAY_ID', '' ); +} + /* ── Global test state ─────────────────────────────────────────────────── */ $GLOBALS['prv_test_state'] = [ @@ -54,6 +78,7 @@ $GLOBALS['prv_test_state'] = [ 'actions' => [], 'wpdb_insert_id' => 1, 'wpdb_results' => [], + 'wpdb_results_queue' => [], 'wpdb_var' => null, 'wpdb_row' => null, 'cron_events' => [], @@ -70,6 +95,7 @@ function prv_test_reset(): void { 'actions' => [], 'wpdb_insert_id' => 1, 'wpdb_results' => [], + 'wpdb_results_queue' => [], 'wpdb_var' => null, 'wpdb_row' => null, 'cron_events' => [], @@ -265,6 +291,10 @@ class stdClass_wpdb { } public function update( string $table, array $data, array $where, array $df = [], array $wf = [] ): int { return 1; } public function get_results( string $query, string $output = 'OBJECT' ): array { + // Queue mode: if wpdb_results_queue is non-empty, shift one result set. + if ( ! empty( $GLOBALS['prv_test_state']['wpdb_results_queue'] ) ) { + return (array) array_shift( $GLOBALS['prv_test_state']['wpdb_results_queue'] ); + } return is_array( $GLOBALS['prv_test_state']['wpdb_results'] ) ? $GLOBALS['prv_test_state']['wpdb_results'] : []; } public function get_row( string $query, string $output = 'OBJECT' ): mixed { return $GLOBALS['prv_test_state']['wpdb_row'] ?? null; } diff --git a/tests/unit/CaptureWriterTest.php b/tests/unit/CaptureWriterTest.php index 9a039b5..3089b94 100644 --- a/tests/unit/CaptureWriterTest.php +++ b/tests/unit/CaptureWriterTest.php @@ -120,21 +120,18 @@ class CaptureWriterTest extends TestCase { $this->assertSame( 'what is TB-500?', $err_io['prompt_text'], 'Error record stores rendered query as prompt_text' ); } - // ── Test 4 (P0 SECURITY — executor path): sentinel must not leak ────── + // ── Test 4 (P0 SECURITY — executor path): configured key must not store in rows ── /** - * Defines PRV_OPENROUTER_API_KEY as a process-scoped PHP constant. - * Must run in a separate process to prevent that constant from bleeding - * into sibling test classes (e.g. KeyStoreTest) within the same PHPUnit run. + * Verifies the executor path does not store the configured API key value + * (PRV_OPENROUTER_API_KEY, defined in bootstrap) in any DB row on a 401. * - * @runInSeparateProcess - * @preserveGlobalState disabled + * The test key 'sk-or-test-key' is defined in tests/bootstrap.php so it is + * available for ALL tests without per-class define() calls. This test asserts + * the value never appears in prv_call_meta or prv_call_io rows, verifying P0 + * security invariant: key does not leak through stored data regardless of path. */ public function test_p0_executor_path_sentinel_key_does_not_leak(): void { - if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) { - define( 'PRV_OPENROUTER_API_KEY', 'sk-or-TEST-LEAK-CANARY' ); - } - $GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 5.0; $GLOBALS['prv_test_state']['options']['prv_peptides'] = array( array( 'slug' => 'bpc-157', 'label' => 'BPC-157' ) ); $GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = array( 'what is {peptide}' ); @@ -166,7 +163,7 @@ class CaptureWriterTest extends TestCase { $all_io = $GLOBALS['prv_test_state']['wpdb_call_io_rows']; $all_json = wp_json_encode( $all_meta ) . wp_json_encode( $all_io ); - $this->assertStringNotContainsString( 'sk-or-TEST-LEAK-CANARY', $all_json, 'Sentinel key does not appear in any stored row (executor path)' ); + $this->assertStringNotContainsString( 'sk-or-test-key', $all_json, 'Configured key does not appear in any stored row (executor path)' ); $this->assertStringNotContainsString( 'Bearer', $all_json, 'Bearer does not appear in any stored row (executor path)' ); $this->assertStringNotContainsString( 'Authorization', $all_json, 'Authorization does not appear in any stored row (executor path)' ); $this->assertGreaterThanOrEqual( 1, count( $all_meta ), 'Executor path wrote at least one call_meta row on 401' ); diff --git a/tests/unit/ConfigVersionTest.php b/tests/unit/ConfigVersionTest.php index d95dd21..4f78830 100644 --- a/tests/unit/ConfigVersionTest.php +++ b/tests/unit/ConfigVersionTest.php @@ -16,15 +16,6 @@ class ConfigVersionTest extends TestCase { protected function setUp(): void { prv_test_reset(); - if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) { - define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' ); - } - if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) { - define( 'PRV_CF_ACCOUNT_ID', '' ); - } - if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) { - define( 'PRV_CF_GATEWAY_ID', '' ); - } } public function test_maybe_seed_initial_version_creates_v1(): void { diff --git a/tests/unit/DashboardPanelTest.php b/tests/unit/DashboardPanelTest.php index 0451a24..775eda2 100644 --- a/tests/unit/DashboardPanelTest.php +++ b/tests/unit/DashboardPanelTest.php @@ -76,9 +76,16 @@ class DashboardPanelTest extends TestCase { } public function test_collector_trendline_includes_config_version(): void { - $GLOBALS['prv_test_state']['wpdb_results'] = [ - [ 'run_id' => 'run-001', 'captured_at' => '2026-06-01 10:00:00', 'cited_count' => 2, 'total_count' => 10, 'position_sum' => 0.5, 'config_version' => 1 ], - [ 'run_id' => 'run-002', 'captured_at' => '2026-06-08 10:00:00', 'cited_count' => 3, 'total_count' => 10, 'position_sum' => 0.6, 'config_version' => 2 ], + // Collector calls get_results() twice: (1) trendline, (2) standings. + // Use wpdb_results_queue to provide separate row sets for each call. + $GLOBALS['prv_test_state']['wpdb_results_queue'] = [ + // 1st call: trendline rows. + [ + [ 'run_id' => 'run-001', 'captured_at' => '2026-06-01 10:00:00', 'cited_count' => 2, 'total_count' => 10, 'position_sum' => 0.5, 'config_version' => 1 ], + [ 'run_id' => 'run-002', 'captured_at' => '2026-06-08 10:00:00', 'cited_count' => 3, 'total_count' => 10, 'position_sum' => 0.6, 'config_version' => 2 ], + ], + // 2nd call: standings rows (empty — not needed for this assertion). + [], ]; $GLOBALS['prv_test_state']['wpdb_var'] = '2026-06-08 10:00:00'; @@ -91,6 +98,7 @@ class DashboardPanelTest extends TestCase { } public function test_collector_payload_has_new_keys(): void { + // Both get_results() calls return [] (no data needed for key-presence check). $GLOBALS['prv_test_state']['wpdb_results'] = []; $GLOBALS['prv_test_state']['wpdb_var'] = null; @@ -102,8 +110,11 @@ class DashboardPanelTest extends TestCase { } public function test_null_config_version_preserved(): void { - $GLOBALS['prv_test_state']['wpdb_results'] = [ - [ 'run_id' => 'run-001', 'captured_at' => '2026-06-01 10:00:00', 'cited_count' => 1, 'total_count' => 5, 'position_sum' => 0.25, 'config_version' => null ], + $GLOBALS['prv_test_state']['wpdb_results_queue'] = [ + // 1st call: trendline rows. + [ [ 'run_id' => 'run-001', 'captured_at' => '2026-06-01 10:00:00', 'cited_count' => 1, 'total_count' => 5, 'position_sum' => 0.25, 'config_version' => null ] ], + // 2nd call: standings rows (empty). + [], ]; $GLOBALS['prv_test_state']['wpdb_var'] = '2026-06-01 10:00:00'; diff --git a/tests/unit/KeyStoreTest.php b/tests/unit/KeyStoreTest.php index 8b24bda..b7e2f5c 100644 --- a/tests/unit/KeyStoreTest.php +++ b/tests/unit/KeyStoreTest.php @@ -115,23 +115,27 @@ class KeyStoreTest extends TestCase { // ── Group 3: Resolver precedence — constant > option > none ────────── - public function test_get_key_returns_empty_with_no_source(): void { - $this->assertSame( '', PRV_Key_Store::get_key() ); + public function test_get_key_returns_constant_value(): void { + // PRV_OPENROUTER_API_KEY='sk-or-test-key' is defined in bootstrap. + // get_key() returns the constant value (highest precedence). + $this->assertSame( 'sk-or-test-key', PRV_Key_Store::get_key(), 'get_key() returns the wp-config constant value' ); } - public function test_get_key_returns_decrypted_from_option(): void { + public function test_get_key_constant_wins_over_option(): void { + // store_key() writes a different plaintext to the encrypted option. + // But get_key() still returns the constant because constant > option. $plaintext = 'sk-or-test-key-value-abc123'; PRV_Key_Store::store_key( $plaintext ); - $this->assertSame( $plaintext, PRV_Key_Store::get_key() ); + $this->assertSame( 'sk-or-test-key', PRV_Key_Store::get_key(), 'Constant wins over stored option (constant > option precedence)' ); } // ── Group 4: Write-only — key value never in rendered HTML output ───── public function test_rendered_html_never_contains_key(): void { - $plaintext = 'sk-or-test-key-value-abc123'; - PRV_Key_Store::store_key( $plaintext ); - + // P0 invariant: key value NEVER appears in any rendered HTML output. + // With PRV_OPENROUTER_API_KEY constant in bootstrap, source = SOURCE_CONSTANT. + // The renderer must still render the password input with value="" (write-only). require_once PRV_PLUGIN_DIR . 'includes/core/class-prv-settings-page.php'; require_once PRV_PLUGIN_DIR . 'includes/core/class-prv-key-manager-renderer.php'; @@ -140,10 +144,10 @@ class KeyStoreTest extends TestCase { $html = (string) ob_get_clean(); $this->assertNotEmpty( $html, 'Key manager card renders non-empty HTML' ); - $this->assertStringNotContainsString( $plaintext, $html, 'Plaintext key absent from rendered HTML' ); - $this->assertStringNotContainsString( 'sk-or-test', $html, 'Key value prefix absent from rendered HTML' ); + // The constant key 'sk-or-test-key' must NOT appear in the HTML (P0 write-only). + $this->assertStringNotContainsString( 'sk-or-test-key', $html, 'Constant API key value absent from rendered HTML (P0)' ); $this->assertStringContainsString( 'type="password"', $html, 'Password input is present in output' ); - $this->assertStringContainsString( 'value=""', $html, 'Password input has empty value attribute' ); + $this->assertStringContainsString( 'value=""', $html, 'Password input has empty value attribute (write-only)' ); $this->assertStringNotContainsString( 'value="sk-', $html, 'No key value in any value= attribute' ); } diff --git a/tests/unit/ProbeRunnerTest.php b/tests/unit/ProbeRunnerTest.php index 9366879..9632014 100644 --- a/tests/unit/ProbeRunnerTest.php +++ b/tests/unit/ProbeRunnerTest.php @@ -37,15 +37,6 @@ class ProbeRunnerTest extends TestCase { protected function setUp(): void { prv_test_reset(); - if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) { - define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' ); - } - if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) { - define( 'PRV_CF_ACCOUNT_ID', '' ); - } - if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) { - define( 'PRV_CF_GATEWAY_ID', '' ); - } $this->sonar_row = [ 'id' => 'mdl_t1', 'slug' => 'perplexity/sonar', 'provider' => 'perplexity', 'enabled' => true, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ]; $this->cited_body = json_encode( [ 'choices' => [ [ 'message' => [ 'content' => 'BPC-157 info.' ] ] ], 'citations' => [ 'https://peptiderepo.com/bpc-157/', 'https://examine.com' ], 'usage' => [ 'total_tokens' => 100 ] ] ); } diff --git a/tests/unit/ProjectedCostTest.php b/tests/unit/ProjectedCostTest.php index c30df39..3fe571d 100644 --- a/tests/unit/ProjectedCostTest.php +++ b/tests/unit/ProjectedCostTest.php @@ -23,15 +23,6 @@ class ProjectedCostTest extends TestCase { protected function setUp(): void { prv_test_reset(); - if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) { - define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' ); - } - if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) { - define( 'PRV_CF_ACCOUNT_ID', '' ); - } - if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) { - define( 'PRV_CF_GATEWAY_ID', '' ); - } } public function test_projected_cost_reflects_enabled_models_x_peptides_x_intents(): void { diff --git a/tests/unit/RunLockTest.php b/tests/unit/RunLockTest.php index f09a697..70ce9c4 100644 --- a/tests/unit/RunLockTest.php +++ b/tests/unit/RunLockTest.php @@ -16,15 +16,6 @@ class RunLockTest extends TestCase { protected function setUp(): void { prv_test_reset(); - if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) { - define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' ); - } - if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) { - define( 'PRV_CF_ACCOUNT_ID', '' ); - } - if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) { - define( 'PRV_CF_GATEWAY_ID', '' ); - } } public function test_acquire_returns_true_when_free(): void { diff --git a/tests/unit/UninstallTest.php b/tests/unit/UninstallTest.php index 4497ffd..75b2346 100644 --- a/tests/unit/UninstallTest.php +++ b/tests/unit/UninstallTest.php @@ -17,15 +17,6 @@ class UninstallTest extends TestCase { protected function setUp(): void { prv_test_reset(); - if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) { - define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' ); - } - if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) { - define( 'PRV_CF_ACCOUNT_ID', '' ); - } - if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) { - define( 'PRV_CF_GATEWAY_ID', '' ); - } } public function test_uninstall_purges_all_prv_options(): void {