Adopt reusable CI standard + PHPUnit conversion (#22)
Squash-merge of PR #22: adopt shared reusable CI workflow, convert PHPUnit to stubs, update branch-protection contexts.
QA-approved at 05d8094a1d. All required CI checks green.
Agent-Session: 2026-06-17-prcore-ci-deploy
This commit is contained in:
285
tests/unit/JsonldPeptidePieceTest.php
Normal file
285
tests/unit/JsonldPeptidePieceTest.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
/**
|
||||
* Regression tests for v0.6.2: inject_graph_pieces (plain array) fatal on Yoast.
|
||||
*
|
||||
* Ported from tests/unit/test-jsonld-peptide-piece.php.
|
||||
*
|
||||
* Root cause: v0.6.0/v0.6.1 hooked wpseo_schema_graph_pieces and appended plain
|
||||
* PHP arrays. Yoast's filter_graph_pieces_to_generate() called get_class() on
|
||||
* every piece, causing: "PHP Fatal: get_class(): Argument #1 ($object) must be
|
||||
* of type object, array given".
|
||||
*
|
||||
* Fix: inject via wpseo_schema_graph (priority 12) using inject_graph_nodes().
|
||||
*
|
||||
* @package PeptideRepoCore\Tests
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for PR_Core_Jsonld::inject_graph_nodes() and related regression.
|
||||
*/
|
||||
class JsonldPeptidePieceTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['pr_test_postmeta'] = [];
|
||||
$GLOBALS['pr_test_is_singular'] = false;
|
||||
$GLOBALS['pr_test_singular_type'] = '';
|
||||
$GLOBALS['pr_test_the_id'] = 0;
|
||||
$GLOBALS['pr_test_peptide_dto'] = null;
|
||||
$GLOBALS['pr_core_test_state'] = [
|
||||
'existing_post_types' => [],
|
||||
'existing_taxonomies' => [],
|
||||
'registered_post_types' => [],
|
||||
'registered_taxonomies' => [],
|
||||
'registered_meta' => [],
|
||||
'added_actions' => [],
|
||||
'added_filters' => [],
|
||||
];
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-schema-sanitizers.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/dto/class-pr-core-peptide-dto.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-drug.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-faq.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php';
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────.
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overrides
|
||||
*/
|
||||
private function make_dto( array $overrides = [] ): PR_Core_Peptide_DTO {
|
||||
return new PR_Core_Peptide_DTO( array_merge( [
|
||||
'id' => 36,
|
||||
'title' => 'BPC-157',
|
||||
'slug' => 'bpc-157',
|
||||
'content' => '',
|
||||
'excerpt' => 'A synthetic pentadecapeptide.',
|
||||
'status' => 'publish',
|
||||
'display_name' => 'BPC-157',
|
||||
'aliases' => [],
|
||||
'molecular_formula' => '',
|
||||
'molecular_weight' => 0.0,
|
||||
'cas_number' => '',
|
||||
'drugbank_id' => '',
|
||||
'chembl_id' => '',
|
||||
'evidence_strength' => 'preclinical',
|
||||
'editorial_review_status' => 'published',
|
||||
'last_editorial_review_at' => '',
|
||||
'medical_editor_id' => 0,
|
||||
'categories' => [],
|
||||
'families' => [],
|
||||
], $overrides ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function make_existing_graph(): array {
|
||||
return [
|
||||
[
|
||||
'@type' => 'MedicalWebPage',
|
||||
'@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage',
|
||||
'name' => 'BPC-157',
|
||||
],
|
||||
[
|
||||
'@type' => 'BreadcrumbList',
|
||||
'@id' => 'https://peptiderepo.com/peptides/bpc-157/#breadcrumb',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function set_peptide_context( int $post_id, array $meta = [] ): void {
|
||||
$GLOBALS['pr_test_the_id'] = $post_id;
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
$GLOBALS['pr_test_singular_type'] = PR_Core_Peptide_CPT::POST_TYPE;
|
||||
$GLOBALS['pr_test_postmeta'][ $post_id ] = $meta;
|
||||
$GLOBALS['pr_test_peptide_dto'] = $this->make_dto( [ 'id' => $post_id ] );
|
||||
// Supply a WP_Post object so PR_Core_Peptide_Repository::find_by_id() passes.
|
||||
// the post_type check and post_to_dto() type hint (our get_post() stub reads.
|
||||
// from pr_test_posts).
|
||||
$post = new WP_Post();
|
||||
$post->ID = $post_id;
|
||||
$post->post_type = PR_Core_Peptide_CPT::POST_TYPE;
|
||||
$post->post_title = 'BPC-157';
|
||||
$post->post_name = 'bpc-157';
|
||||
$post->post_content = '';
|
||||
$post->post_excerpt = '';
|
||||
$post->post_status = 'publish';
|
||||
$GLOBALS['pr_test_posts'][ $post_id ] = $post;
|
||||
}
|
||||
|
||||
// ── Regression: get_class(array) throws ───────────────────────────.
|
||||
|
||||
public function test_get_class_on_plain_array_throws(): void {
|
||||
$GLOBALS['pr_test_postmeta'][36] = [];
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $this->make_dto() );
|
||||
$caught = false;
|
||||
try {
|
||||
get_class( $drug_node );
|
||||
} catch ( TypeError $e ) {
|
||||
$caught = true;
|
||||
} catch ( Error $e ) {
|
||||
$caught = true;
|
||||
}
|
||||
$this->assertTrue( $caught, 'get_class(array) must throw — confirms the v0.6.1 fatal' );
|
||||
}
|
||||
|
||||
// ── inject_graph_nodes does not throw ─────────────────────────────.
|
||||
|
||||
public function test_inject_graph_nodes_does_not_throw(): void {
|
||||
$this->set_peptide_context( 36, [
|
||||
'_pr_molecular_formula' => 'C62H98N16O22',
|
||||
'_pr_molecular_weight' => '1419.5',
|
||||
'_pr_aliases' => '["Body Protection Compound"]',
|
||||
] );
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$threw = false;
|
||||
try {
|
||||
$jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
|
||||
} catch ( Throwable $e ) {
|
||||
$threw = true;
|
||||
}
|
||||
$this->assertFalse( $threw );
|
||||
}
|
||||
|
||||
public function test_inject_graph_nodes_appends_drug_node(): void {
|
||||
$this->set_peptide_context( 36, [
|
||||
'_pr_molecular_formula' => 'C62H98N16O22',
|
||||
'_pr_molecular_weight' => '1419.5',
|
||||
'_pr_aliases' => '["Body Protection Compound"]',
|
||||
] );
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
|
||||
$this->assertGreaterThan( count( $this->make_existing_graph() ), count( $result ) );
|
||||
}
|
||||
|
||||
public function test_inject_graph_nodes_exactly_one_drug_node(): void {
|
||||
$this->set_peptide_context( 36, [
|
||||
'_pr_molecular_formula' => 'C62H98N16O22',
|
||||
'_pr_molecular_weight' => '1419.5',
|
||||
'_pr_aliases' => '["Body Protection Compound"]',
|
||||
] );
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
|
||||
$drug_nodes = array_filter( $result, static function ( $n ) {
|
||||
$types = is_array( $n['@type'] ?? null ) ? $n['@type'] : [ $n['@type'] ?? '' ];
|
||||
return in_array( 'Drug', $types, true );
|
||||
} );
|
||||
$this->assertCount( 1, $drug_nodes );
|
||||
}
|
||||
|
||||
public function test_inject_graph_nodes_drug_id_format(): void {
|
||||
$this->set_peptide_context( 36, [
|
||||
'_pr_molecular_formula' => 'C62H98N16O22',
|
||||
'_pr_molecular_weight' => '1419.5',
|
||||
'_pr_aliases' => '[]',
|
||||
] );
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
|
||||
$drug_nodes = array_values( array_filter( $result, static function ( $n ) {
|
||||
$types = is_array( $n['@type'] ?? null ) ? $n['@type'] : [ $n['@type'] ?? '' ];
|
||||
return in_array( 'Drug', $types, true );
|
||||
} ) );
|
||||
$this->assertSame( 'https://peptiderepo.com/peptides/bpc-157/#drug', $drug_nodes[0]['@id'] );
|
||||
}
|
||||
|
||||
public function test_inject_graph_nodes_drug_has_molecular_formula(): void {
|
||||
$this->set_peptide_context( 36, [
|
||||
'_pr_molecular_formula' => 'C62H98N16O22',
|
||||
'_pr_molecular_weight' => '1419.5',
|
||||
'_pr_aliases' => '[]',
|
||||
] );
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
|
||||
$drug_nodes = array_values( array_filter( $result, static function ( $n ) {
|
||||
$types = is_array( $n['@type'] ?? null ) ? $n['@type'] : [ $n['@type'] ?? '' ];
|
||||
return in_array( 'Drug', $types, true );
|
||||
} ) );
|
||||
$this->assertArrayHasKey( 'molecularFormula', $drug_nodes[0] );
|
||||
}
|
||||
|
||||
public function test_inject_graph_nodes_drug_molecular_weight_quantitative_value(): void {
|
||||
$this->set_peptide_context( 36, [ '_pr_molecular_weight' => '1419.5' ] );
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
|
||||
$drug_nodes = array_values( array_filter( $result, static function ( $n ) {
|
||||
$types = is_array( $n['@type'] ?? null ) ? $n['@type'] : [ $n['@type'] ?? '' ];
|
||||
return in_array( 'Drug', $types, true );
|
||||
} ) );
|
||||
$this->assertSame( 'QuantitativeValue', $drug_nodes[0]['molecularWeight']['@type'] );
|
||||
$this->assertSame( 1419.5, $drug_nodes[0]['molecularWeight']['value'] );
|
||||
$this->assertSame( 'g/mol', $drug_nodes[0]['molecularWeight']['unitText'] );
|
||||
}
|
||||
|
||||
public function test_inject_graph_nodes_preserves_webpage_and_breadcrumb(): void {
|
||||
$this->set_peptide_context( 36, [] );
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
|
||||
$webpage = array_filter( $result, static fn( $n ) => ( $n['@type'] ?? '' ) === 'MedicalWebPage' );
|
||||
$breadcrumb = array_filter( $result, static fn( $n ) => ( $n['@type'] ?? '' ) === 'BreadcrumbList' );
|
||||
$this->assertCount( 1, $webpage );
|
||||
$this->assertCount( 1, $breadcrumb );
|
||||
}
|
||||
|
||||
// ── FAQ injection via inject_graph_nodes ──────────────────────────.
|
||||
|
||||
public function test_inject_graph_nodes_faq_present_when_items_exist(): void {
|
||||
$this->set_peptide_context( 36, [
|
||||
'_pr_faq_items' => wp_json_encode( [
|
||||
[ 'question' => 'What is BPC-157?', 'answer' => 'A synthetic pentadecapeptide.' ],
|
||||
[ 'question' => 'Is it safe?', 'answer' => 'Research use only.' ],
|
||||
] ),
|
||||
] );
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
|
||||
$faq_nodes = array_filter( $result, static fn( $n ) => ( $n['@type'] ?? '' ) === 'FAQPage' );
|
||||
$this->assertCount( 1, $faq_nodes );
|
||||
$faq_node = array_values( $faq_nodes )[0];
|
||||
$this->assertSame( 'https://peptiderepo.com/peptides/bpc-157/#faq', $faq_node['@id'] );
|
||||
$this->assertCount( 2, $faq_node['mainEntity'] );
|
||||
}
|
||||
|
||||
public function test_inject_graph_nodes_faq_absent_when_items_empty(): void {
|
||||
$this->set_peptide_context( 36, [ '_pr_faq_items' => '[]' ] );
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$result = $jsonld->inject_graph_nodes( $this->make_existing_graph(), null );
|
||||
$faq_nodes = array_filter( $result, static fn( $n ) => ( $n['@type'] ?? '' ) === 'FAQPage' );
|
||||
$this->assertCount( 0, $faq_nodes );
|
||||
}
|
||||
|
||||
// ── Non-peptide page: no injection ────────────────────────────────.
|
||||
|
||||
public function test_inject_graph_nodes_noop_on_non_peptide_page(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = false;
|
||||
$GLOBALS['pr_test_singular_type'] = '';
|
||||
$GLOBALS['pr_test_the_id'] = 0;
|
||||
$non_peptide_graph = [ [ '@type' => 'WebPage', '@id' => 'https://peptiderepo.com/about/#webpage' ] ];
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
$result = $jsonld->inject_graph_nodes( $non_peptide_graph, null );
|
||||
$this->assertCount( 1, $result );
|
||||
}
|
||||
|
||||
// ── register_hooks: wpseo_schema_graph_pieces must NOT be hooked ──.
|
||||
|
||||
public function test_jsonld_does_not_hook_wpseo_schema_graph_pieces(): void {
|
||||
$src = file_get_contents( PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php' );
|
||||
$this->assertStringNotContainsString(
|
||||
"'wpseo_schema_graph_pieces'",
|
||||
$src,
|
||||
'class-pr-core-jsonld.php must NOT hook wpseo_schema_graph_pieces (v0.6.2 fix)'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_jsonld_references_inject_graph_nodes(): void {
|
||||
$src = file_get_contents( PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php' );
|
||||
$this->assertStringContainsString( 'inject_graph_nodes', $src );
|
||||
}
|
||||
|
||||
public function test_jsonld_hooks_wpseo_schema_graph(): void {
|
||||
$src = file_get_contents( PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php' );
|
||||
$this->assertStringContainsString( "'wpseo_schema_graph'", $src );
|
||||
}
|
||||
}
|
||||
297
tests/unit/JsonldTest.php
Normal file
297
tests/unit/JsonldTest.php
Normal file
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
/**
|
||||
* Unit tests for JSON-LD v0.6.0 emission.
|
||||
*
|
||||
* Ported from tests/unit/test-jsonld.php.
|
||||
*
|
||||
* Coverage:
|
||||
* - Drug node: @id, @type, fields present/absent based on meta.
|
||||
* - FAQPage: present/absent, @id, mainEntity count.
|
||||
* - MedicalWebPage retype on/off peptide singular.
|
||||
* - No duplicate WebPage/BreadcrumbList nodes from our builders.
|
||||
* - PR_Core_Schema_Sanitizers::sanitize_faq_items() validation.
|
||||
*
|
||||
* @package PeptideRepoCore\Tests
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for JSON-LD Drug/FAQ/Webpage emission.
|
||||
*/
|
||||
class JsonldTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['pr_test_postmeta'] = [];
|
||||
$GLOBALS['pr_test_is_singular'] = false;
|
||||
$GLOBALS['pr_test_singular_type'] = '';
|
||||
$GLOBALS['pr_test_the_id'] = 0;
|
||||
$GLOBALS['pr_core_test_state'] = [
|
||||
'existing_post_types' => [],
|
||||
'existing_taxonomies' => [],
|
||||
'registered_post_types' => [],
|
||||
'registered_taxonomies' => [],
|
||||
'registered_meta' => [],
|
||||
'added_actions' => [],
|
||||
'added_filters' => [],
|
||||
];
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-schema-sanitizers.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/dto/class-pr-core-peptide-dto.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-drug.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-faq.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php';
|
||||
}
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────────.
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overrides
|
||||
*/
|
||||
private function make_peptide_dto( array $overrides = [] ): PR_Core_Peptide_DTO {
|
||||
return new PR_Core_Peptide_DTO( array_merge( [
|
||||
'id' => 1,
|
||||
'title' => 'BPC-157',
|
||||
'slug' => 'bpc-157',
|
||||
'content' => '',
|
||||
'excerpt' => 'A synthetic peptide.',
|
||||
'status' => 'publish',
|
||||
'display_name' => 'BPC-157',
|
||||
'aliases' => [],
|
||||
'molecular_formula' => '',
|
||||
'molecular_weight' => 0.0,
|
||||
'cas_number' => '',
|
||||
'drugbank_id' => '',
|
||||
'chembl_id' => '',
|
||||
'evidence_strength' => 'preclinical',
|
||||
'editorial_review_status' => 'published',
|
||||
'last_editorial_review_at' => '',
|
||||
'medical_editor_id' => 0,
|
||||
'categories' => [],
|
||||
'families' => [],
|
||||
], $overrides ) );
|
||||
}
|
||||
|
||||
// ── Drug @id ─────────────────────────────────────────────────────.
|
||||
|
||||
public function test_drug_node_has_id(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertArrayHasKey( '@id', $drug_node );
|
||||
}
|
||||
|
||||
public function test_drug_node_id_format(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertSame( 'https://peptiderepo.com/peptides/bpc-157/#drug', $drug_node['@id'] );
|
||||
}
|
||||
|
||||
// ── Drug @type ────────────────────────────────────────────────────.
|
||||
|
||||
public function test_drug_type_is_array(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertIsArray( $drug_node['@type'] );
|
||||
}
|
||||
|
||||
public function test_drug_type_includes_drug(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertContains( 'Drug', $drug_node['@type'] );
|
||||
}
|
||||
|
||||
public function test_drug_type_includes_molecular_entity(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertContains( 'MolecularEntity', $drug_node['@type'] );
|
||||
}
|
||||
|
||||
// ── Drug fields present when meta set ────────────────────────────.
|
||||
|
||||
public function test_molecular_formula_present_when_meta_set(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [ '_pr_molecular_formula' => 'C62H98N16O22', '_pr_molecular_weight' => '1419.5', '_pr_aliases' => '[]' ];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertArrayHasKey( 'molecularFormula', $drug_node );
|
||||
$this->assertSame( 'C62H98N16O22', $drug_node['molecularFormula'] );
|
||||
}
|
||||
|
||||
public function test_molecular_weight_present_when_meta_set(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [ '_pr_molecular_formula' => '', '_pr_molecular_weight' => '1419.5', '_pr_aliases' => '[]' ];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertArrayHasKey( 'molecularWeight', $drug_node );
|
||||
$this->assertSame( 'QuantitativeValue', $drug_node['molecularWeight']['@type'] );
|
||||
$this->assertSame( 1419.5, $drug_node['molecularWeight']['value'] );
|
||||
$this->assertSame( 'g/mol', $drug_node['molecularWeight']['unitText'] );
|
||||
}
|
||||
|
||||
public function test_alternate_name_present_when_aliases_set(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [ '_pr_molecular_formula' => '', '_pr_molecular_weight' => '0', '_pr_aliases' => '["Body Protection Compound-157","PL 14736"]' ];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertArrayHasKey( 'alternateName', $drug_node );
|
||||
$this->assertCount( 2, $drug_node['alternateName'] );
|
||||
}
|
||||
|
||||
// ── Drug fields omitted when meta empty ──────────────────────────.
|
||||
|
||||
public function test_molecular_formula_omitted_when_meta_empty(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertArrayNotHasKey( 'molecularFormula', $drug_node );
|
||||
}
|
||||
|
||||
public function test_molecular_weight_omitted_when_meta_empty(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertArrayNotHasKey( 'molecularWeight', $drug_node );
|
||||
}
|
||||
|
||||
public function test_alternate_name_omitted_when_aliases_empty(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [ '_pr_aliases' => '[]' ];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$this->assertArrayNotHasKey( 'alternateName', $drug_node );
|
||||
}
|
||||
|
||||
// ── FAQPage emission ──────────────────────────────────────────────.
|
||||
|
||||
public function test_faq_piece_not_null_when_items_present(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [
|
||||
'_pr_faq_items' => wp_json_encode( [
|
||||
[ 'question' => 'What is BPC-157?', 'answer' => 'A synthetic peptide.' ],
|
||||
[ 'question' => 'Is it legal?', 'answer' => 'Research use only.' ],
|
||||
] ),
|
||||
];
|
||||
$faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
|
||||
$this->assertNotNull( $faq_piece );
|
||||
}
|
||||
|
||||
public function test_faq_piece_type_is_faq_page(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [
|
||||
'_pr_faq_items' => wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ),
|
||||
];
|
||||
$faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
|
||||
$this->assertSame( 'FAQPage', $faq_piece['@type'] );
|
||||
}
|
||||
|
||||
public function test_faq_piece_id_format(): void {
|
||||
$permalink = 'https://peptiderepo.com/peptides/bpc-157/';
|
||||
$GLOBALS['pr_test_postmeta'][1] = [
|
||||
'_pr_faq_items' => wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ),
|
||||
];
|
||||
$faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, $permalink );
|
||||
$this->assertSame( $permalink . '#faq', $faq_piece['@id'] );
|
||||
}
|
||||
|
||||
public function test_faq_piece_has_main_entity(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [
|
||||
'_pr_faq_items' => wp_json_encode( [
|
||||
[ 'question' => 'What is BPC-157?', 'answer' => 'A synthetic peptide.' ],
|
||||
[ 'question' => 'Is it legal?', 'answer' => 'Research use only.' ],
|
||||
] ),
|
||||
];
|
||||
$faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
|
||||
$this->assertArrayHasKey( 'mainEntity', $faq_piece );
|
||||
$this->assertCount( 2, $faq_piece['mainEntity'] );
|
||||
}
|
||||
|
||||
public function test_faq_main_entity_question_type(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [
|
||||
'_pr_faq_items' => wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ),
|
||||
];
|
||||
$faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
|
||||
$this->assertSame( 'Question', $faq_piece['mainEntity'][0]['@type'] );
|
||||
}
|
||||
|
||||
public function test_faq_main_entity_has_accepted_answer(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [
|
||||
'_pr_faq_items' => wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ),
|
||||
];
|
||||
$faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
|
||||
$this->assertArrayHasKey( 'acceptedAnswer', $faq_piece['mainEntity'][0] );
|
||||
}
|
||||
|
||||
public function test_faq_piece_null_when_items_json_empty(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [ '_pr_faq_items' => '[]' ];
|
||||
$faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
|
||||
$this->assertNull( $faq_piece );
|
||||
}
|
||||
|
||||
public function test_faq_piece_null_when_items_absent(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$faq_piece = ( new PR_Core_Jsonld_Faq() )->build( 1, 'https://peptiderepo.com/peptides/bpc-157/' );
|
||||
$this->assertNull( $faq_piece );
|
||||
}
|
||||
|
||||
// ── MedicalWebPage retype ─────────────────────────────────────────.
|
||||
|
||||
public function test_retype_returns_medical_webpage_on_peptide_singular(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
$GLOBALS['pr_test_singular_type'] = 'peptide';
|
||||
$result = ( new PR_Core_Jsonld_Webpage() )->retype_to_medical_webpage( 'WebPage' );
|
||||
$this->assertSame( 'MedicalWebPage', $result );
|
||||
}
|
||||
|
||||
public function test_retype_passes_through_on_non_peptide_page(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = false;
|
||||
$GLOBALS['pr_test_singular_type'] = '';
|
||||
$result = ( new PR_Core_Jsonld_Webpage() )->retype_to_medical_webpage( 'WebPage' );
|
||||
$this->assertSame( 'WebPage', $result );
|
||||
}
|
||||
|
||||
// ── No duplicate nodes from our builders ──────────────────────────.
|
||||
|
||||
public function test_builders_do_not_emit_webpage_type(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [
|
||||
'_pr_molecular_formula' => 'C62H98N16O22',
|
||||
'_pr_molecular_weight' => '1419.5',
|
||||
'_pr_aliases' => '["Body Protection Compound-157","PL 14736"]',
|
||||
];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$types = is_array( $drug_node['@type'] ) ? $drug_node['@type'] : [ $drug_node['@type'] ];
|
||||
$this->assertNotContains( 'WebPage', $types );
|
||||
$this->assertNotContains( 'MedicalWebPage', $types );
|
||||
}
|
||||
|
||||
public function test_builders_do_not_emit_breadcrumb_type(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$peptide = $this->make_peptide_dto();
|
||||
$drug_node = ( new PR_Core_Jsonld_Drug() )->build( $peptide );
|
||||
$types = is_array( $drug_node['@type'] ) ? $drug_node['@type'] : [ $drug_node['@type'] ];
|
||||
$this->assertNotContains( 'BreadcrumbList', $types );
|
||||
}
|
||||
|
||||
// ── sanitize_faq_items() ──────────────────────────────────────────.
|
||||
|
||||
public function test_sanitize_faq_items_valid_input(): void {
|
||||
$result = PR_Core_Schema_Sanitizers::sanitize_faq_items( wp_json_encode( [ [ 'question' => 'Q?', 'answer' => 'A.' ] ] ) );
|
||||
$decoded = json_decode( $result, true );
|
||||
$this->assertIsArray( $decoded );
|
||||
$this->assertCount( 1, $decoded );
|
||||
}
|
||||
|
||||
public function test_sanitize_faq_items_empty_json_array(): void {
|
||||
$this->assertSame( '[]', PR_Core_Schema_Sanitizers::sanitize_faq_items( '[]' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_faq_items_malformed_json(): void {
|
||||
$this->assertSame( '[]', PR_Core_Schema_Sanitizers::sanitize_faq_items( 'not-json' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_faq_items_drops_item_missing_answer(): void {
|
||||
$result = PR_Core_Schema_Sanitizers::sanitize_faq_items( wp_json_encode( [ [ 'question' => 'No answer here?' ] ] ) );
|
||||
$decoded = json_decode( $result, true );
|
||||
$this->assertCount( 0, $decoded );
|
||||
}
|
||||
}
|
||||
112
tests/unit/JsonldWebpageTest.php
Normal file
112
tests/unit/JsonldWebpageTest.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* Regression tests for v0.6.1: Yoast wpseo_schema_webpage_type array contract.
|
||||
*
|
||||
* Ported from tests/unit/test-jsonld-webpage.php.
|
||||
*
|
||||
* Root cause: v0.6.0 retype_to_medical_webpage() was strict-typed string, but
|
||||
* Yoast passes ['WebPage'] (array) on every singular page.
|
||||
* strict_types=1 → TypeError → HTTP 500 on all monographs + plain Pages.
|
||||
*
|
||||
* These tests fail against v0.6.0 and pass after the v0.6.1 fix.
|
||||
*
|
||||
* @package PeptideRepoCore\Tests
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Regression tests for PR_Core_Jsonld_Webpage v0.6.1 array-type contract.
|
||||
*/
|
||||
class JsonldWebpageTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['pr_test_postmeta'] = [];
|
||||
$GLOBALS['pr_test_is_singular'] = false;
|
||||
$GLOBALS['pr_test_singular_type'] = '';
|
||||
$GLOBALS['pr_test_the_id'] = 0;
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
|
||||
}
|
||||
|
||||
// ── retype_to_medical_webpage: array input ────────────────────────.
|
||||
|
||||
public function test_retype_non_peptide_singular_array_unchanged(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
$GLOBALS['pr_test_singular_type'] = 'page';
|
||||
$result = ( new PR_Core_Jsonld_Webpage() )->retype_to_medical_webpage( [ 'WebPage' ] );
|
||||
$this->assertSame( [ 'WebPage' ], $result );
|
||||
}
|
||||
|
||||
public function test_retype_peptide_singular_array_returns_medical_webpage_string(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
$GLOBALS['pr_test_singular_type'] = 'peptide';
|
||||
$result = ( new PR_Core_Jsonld_Webpage() )->retype_to_medical_webpage( [ 'WebPage' ] );
|
||||
$this->assertSame( 'MedicalWebPage', $result );
|
||||
}
|
||||
|
||||
public function test_retype_string_input_non_singular_passthrough(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = false;
|
||||
$GLOBALS['pr_test_singular_type'] = '';
|
||||
$result = ( new PR_Core_Jsonld_Webpage() )->retype_to_medical_webpage( 'CollectionPage' );
|
||||
$this->assertSame( 'CollectionPage', $result );
|
||||
}
|
||||
|
||||
// ── enrich_webpage_piece: @type as array injects enrichments ──────.
|
||||
|
||||
public function test_enrich_webpage_piece_array_type_injects_last_reviewed(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
$GLOBALS['pr_test_singular_type'] = 'peptide';
|
||||
$GLOBALS['pr_test_the_id'] = 42;
|
||||
$GLOBALS['pr_test_postmeta'][42] = [ '_pr_last_reviewed' => '2026-06-01' ];
|
||||
|
||||
$graph = [ [ '@type' => [ 'WebPage' ], '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage', 'name' => 'BPC-157' ] ];
|
||||
$result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
|
||||
$piece = $result_graph[0] ?? [];
|
||||
$this->assertArrayHasKey( 'lastReviewed', $piece );
|
||||
$this->assertSame( '2026-06-01', $piece['lastReviewed'] );
|
||||
}
|
||||
|
||||
public function test_enrich_webpage_piece_array_type_injects_reviewed_by(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
$GLOBALS['pr_test_singular_type'] = 'peptide';
|
||||
$GLOBALS['pr_test_the_id'] = 42;
|
||||
$GLOBALS['pr_test_postmeta'][42] = [ '_pr_last_reviewed' => '2026-06-01' ];
|
||||
|
||||
$graph = [ [ '@type' => [ 'WebPage' ], '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage' ] ];
|
||||
$result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
|
||||
$this->assertArrayHasKey( 'reviewedBy', $result_graph[0] ?? [] );
|
||||
}
|
||||
|
||||
public function test_enrich_webpage_piece_array_type_injects_audience(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
$GLOBALS['pr_test_singular_type'] = 'peptide';
|
||||
$GLOBALS['pr_test_the_id'] = 42;
|
||||
$GLOBALS['pr_test_postmeta'][42] = [ '_pr_last_reviewed' => '2026-06-01' ];
|
||||
|
||||
$graph = [ [ '@type' => [ 'WebPage' ], '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage' ] ];
|
||||
$result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
|
||||
$this->assertArrayHasKey( 'audience', $result_graph[0] ?? [] );
|
||||
}
|
||||
|
||||
public function test_enrich_webpage_piece_string_medical_webpage_also_enriches(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
$GLOBALS['pr_test_singular_type'] = 'peptide';
|
||||
$GLOBALS['pr_test_the_id'] = 42;
|
||||
$GLOBALS['pr_test_postmeta'][42] = [ '_pr_last_reviewed' => '2026-06-01' ];
|
||||
|
||||
$graph = [ [ '@type' => 'MedicalWebPage', '@id' => 'https://peptiderepo.com/peptides/bpc-157/#webpage' ] ];
|
||||
$result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
|
||||
$this->assertArrayHasKey( 'lastReviewed', $result_graph[0] ?? [] );
|
||||
}
|
||||
|
||||
public function test_enrich_webpage_piece_non_peptide_does_not_inject(): void {
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
$GLOBALS['pr_test_singular_type'] = 'page';
|
||||
$GLOBALS['pr_test_the_id'] = 50;
|
||||
$GLOBALS['pr_test_postmeta'][50] = [];
|
||||
|
||||
$graph = [ [ '@type' => [ 'WebPage' ], '@id' => 'https://peptiderepo.com/about/#webpage' ] ];
|
||||
$result_graph = ( new PR_Core_Jsonld_Webpage() )->enrich_webpage_piece( $graph, new stdClass() );
|
||||
$this->assertArrayNotHasKey( 'lastReviewed', $result_graph[0] ?? [] );
|
||||
}
|
||||
}
|
||||
239
tests/unit/Migration0004Test.php
Normal file
239
tests/unit/Migration0004Test.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
/**
|
||||
* Unit tests for PR_Core_Migration_0004_Backfill_Peptide_Meta.
|
||||
*
|
||||
* Ported from tests/unit/test-migration-0004.php.
|
||||
*
|
||||
* Coverage:
|
||||
* - parse_weight(): strips "Da"/"DA"/"da" suffix, handles plain float,
|
||||
* empty string → 0.0, suffix-only → 0.0.
|
||||
* - parse_aliases_string(): splits CSV, trims, drops empty, returns '[]' for empty.
|
||||
* - PR_Core_Schema_Sanitizers::sanitize_molecular_formula(): valid, HTML, groups,
|
||||
* injection, fully-invalid, empty.
|
||||
* - Idempotency: backfill_post() returns 'skipped' when all three keys populated.
|
||||
* - PSA source path: copies and transforms psa_* → _pr_* keys.
|
||||
* - PubChem skip path: 'pubchem_skip' when PubChem network fails.
|
||||
*
|
||||
* @package PeptideRepoCore\Tests
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for PR_Core_Migration_0004_Backfill_Peptide_Meta.
|
||||
*/
|
||||
class Migration0004Test extends TestCase {
|
||||
|
||||
/** @var PR_Core_Migration_0004_Backfill_Peptide_Meta */
|
||||
private $migration;
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['pr_test_postmeta'] = [];
|
||||
$GLOBALS['pr_test_updated_meta'] = [];
|
||||
$GLOBALS['pr_test_posts'] = [];
|
||||
$GLOBALS['pr_core_test_state'] = [
|
||||
'existing_post_types' => [],
|
||||
'existing_taxonomies' => [],
|
||||
'registered_post_types' => [],
|
||||
'registered_taxonomies' => [],
|
||||
'registered_meta' => [],
|
||||
'added_actions' => [],
|
||||
'added_filters' => [],
|
||||
];
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-schema-sanitizers.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/migrations/class-pr-core-pubchem-client.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/migrations/class-pr-core-migration-0004-backfill-peptide-meta.php';
|
||||
$this->migration = new PR_Core_Migration_0004_Backfill_Peptide_Meta();
|
||||
}
|
||||
|
||||
// ── Helper: call private method via reflection ────────────────────.
|
||||
|
||||
/**
|
||||
* @param mixed[] $args
|
||||
* @return mixed
|
||||
*/
|
||||
private function call_private( object $obj, string $method, array $args = [] ) {
|
||||
$ref = new ReflectionMethod( $obj, $method );
|
||||
$ref->setAccessible( true );
|
||||
return $ref->invokeArgs( $obj, $args );
|
||||
}
|
||||
|
||||
// ── parse_weight() ────────────────────────────────────────────────.
|
||||
|
||||
public function test_parse_weight_strips_da_suffix(): void {
|
||||
$this->assertSame( 1419.5, $this->call_private( $this->migration, 'parse_weight', [ '1419.5 Da' ] ) );
|
||||
}
|
||||
|
||||
public function test_parse_weight_strips_uppercase_da(): void {
|
||||
$this->assertSame( 1419.5, $this->call_private( $this->migration, 'parse_weight', [ '1419.5 DA' ] ) );
|
||||
}
|
||||
|
||||
public function test_parse_weight_plain_float(): void {
|
||||
$this->assertSame( 1419.5, $this->call_private( $this->migration, 'parse_weight', [ '1419.5' ] ) );
|
||||
}
|
||||
|
||||
public function test_parse_weight_empty_returns_zero(): void {
|
||||
$this->assertSame( 0.0, $this->call_private( $this->migration, 'parse_weight', [ '' ] ) );
|
||||
}
|
||||
|
||||
public function test_parse_weight_suffix_only_returns_zero(): void {
|
||||
$this->assertSame( 0.0, $this->call_private( $this->migration, 'parse_weight', [ ' Da ' ] ) );
|
||||
}
|
||||
|
||||
public function test_parse_weight_lowercase_da_with_whitespace(): void {
|
||||
$this->assertSame( 800.123, $this->call_private( $this->migration, 'parse_weight', [ ' 800.123 da ' ] ) );
|
||||
}
|
||||
|
||||
// ── parse_aliases_string() ────────────────────────────────────────.
|
||||
|
||||
public function test_parse_aliases_string_splits_csv(): void {
|
||||
$result = json_decode(
|
||||
$this->call_private( $this->migration, 'parse_aliases_string', [ 'Body Protection Compound-157, PL 14736, PL-10, Bepecin' ] ),
|
||||
true
|
||||
);
|
||||
$this->assertIsArray( $result );
|
||||
$this->assertCount( 4, $result );
|
||||
$this->assertSame( 'Body Protection Compound-157', $result[0] );
|
||||
$this->assertSame( 'PL 14736', $result[1] );
|
||||
}
|
||||
|
||||
public function test_parse_aliases_string_empty_returns_json_empty_array(): void {
|
||||
$this->assertSame( '[]', $this->call_private( $this->migration, 'parse_aliases_string', [ '' ] ) );
|
||||
}
|
||||
|
||||
public function test_parse_aliases_string_whitespace_only_returns_json_empty_array(): void {
|
||||
$this->assertSame( '[]', $this->call_private( $this->migration, 'parse_aliases_string', [ ' , , ' ] ) );
|
||||
}
|
||||
|
||||
public function test_parse_aliases_string_single_alias(): void {
|
||||
$result = json_decode( $this->call_private( $this->migration, 'parse_aliases_string', [ 'Only One' ] ), true );
|
||||
$this->assertCount( 1, $result );
|
||||
}
|
||||
|
||||
// ── sanitize_molecular_formula() ──────────────────────────────────.
|
||||
|
||||
public function test_sanitize_formula_valid_passes_unchanged(): void {
|
||||
$this->assertSame( 'C62H98N16O22', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( 'C62H98N16O22' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_formula_strips_html_tags(): void {
|
||||
$this->assertSame( 'C62H98N16O22', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( '<b>C62H98N16O22</b>' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_formula_preserves_parentheses(): void {
|
||||
$this->assertSame( 'C2H5(OH)', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( 'C2H5(OH)' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_formula_strips_non_formula_chars(): void {
|
||||
$this->assertSame( 'C10H12', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( "C10H12\ninjection" ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_formula_fully_invalid_returns_empty(): void {
|
||||
$this->assertSame( '', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( '<script>alert(1)</script>' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_formula_empty_returns_empty(): void {
|
||||
$this->assertSame( '', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( '' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_formula_whitespace_only_returns_empty(): void {
|
||||
$this->assertSame( '', PR_Core_Schema_Sanitizers::sanitize_molecular_formula( ' ' ) );
|
||||
}
|
||||
|
||||
// ── Idempotency ───────────────────────────────────────────────────.
|
||||
|
||||
public function test_backfill_post_skipped_when_all_keys_populated(): void {
|
||||
$GLOBALS['pr_test_postmeta'][1] = [
|
||||
'_pr_molecular_formula' => 'C62H98N16O22',
|
||||
'_pr_molecular_weight' => '1419.5',
|
||||
'_pr_aliases' => '["Alias One"]',
|
||||
];
|
||||
$result = $this->call_private( $this->migration, 'backfill_post', [ 1 ] );
|
||||
$this->assertSame( 'skipped', $result );
|
||||
$this->assertEmpty( $GLOBALS['pr_test_updated_meta'] );
|
||||
}
|
||||
|
||||
// ── PSA source path ───────────────────────────────────────────────.
|
||||
|
||||
public function test_backfill_post_copies_psa_data(): void {
|
||||
$GLOBALS['pr_test_postmeta'][2] = [
|
||||
'psa_molecular_formula' => 'C62H98N16O22',
|
||||
'psa_molecular_weight' => '1419.5 Da',
|
||||
'psa_aliases' => 'Body Protection Compound-157, PL 14736',
|
||||
];
|
||||
$GLOBALS['pr_test_posts'][2] = (object) [ 'post_title' => 'BPC-157', 'ID' => 2 ];
|
||||
$result = $this->call_private( $this->migration, 'backfill_post', [ 2 ] );
|
||||
$this->assertSame( 'copied_psa', $result );
|
||||
}
|
||||
|
||||
public function test_backfill_post_psa_writes_formula_correctly(): void {
|
||||
$GLOBALS['pr_test_postmeta'][2] = [
|
||||
'psa_molecular_formula' => 'C62H98N16O22',
|
||||
'psa_molecular_weight' => '1419.5 Da',
|
||||
'psa_aliases' => 'Body Protection Compound-157',
|
||||
];
|
||||
$GLOBALS['pr_test_posts'][2] = (object) [ 'post_title' => 'BPC-157', 'ID' => 2 ];
|
||||
$this->call_private( $this->migration, 'backfill_post', [ 2 ] );
|
||||
$written = null;
|
||||
foreach ( $GLOBALS['pr_test_updated_meta'] as $entry ) {
|
||||
if ( '_pr_molecular_formula' === $entry['key'] ) {
|
||||
$written = $entry['value'];
|
||||
}
|
||||
}
|
||||
$this->assertSame( 'C62H98N16O22', $written );
|
||||
}
|
||||
|
||||
public function test_backfill_post_psa_strips_da_from_weight(): void {
|
||||
$GLOBALS['pr_test_postmeta'][2] = [
|
||||
'psa_molecular_formula' => 'C62H98N16O22',
|
||||
'psa_molecular_weight' => '1419.5 Da',
|
||||
'psa_aliases' => 'Body Protection Compound-157',
|
||||
];
|
||||
$GLOBALS['pr_test_posts'][2] = (object) [ 'post_title' => 'BPC-157', 'ID' => 2 ];
|
||||
$this->call_private( $this->migration, 'backfill_post', [ 2 ] );
|
||||
$written = null;
|
||||
foreach ( $GLOBALS['pr_test_updated_meta'] as $entry ) {
|
||||
if ( '_pr_molecular_weight' === $entry['key'] ) {
|
||||
$written = $entry['value'];
|
||||
}
|
||||
}
|
||||
$this->assertSame( '1419.5', $written );
|
||||
}
|
||||
|
||||
public function test_backfill_post_psa_writes_aliases_as_json_array(): void {
|
||||
$GLOBALS['pr_test_postmeta'][2] = [
|
||||
'psa_molecular_formula' => 'C62H98N16O22',
|
||||
'psa_molecular_weight' => '1419.5 Da',
|
||||
'psa_aliases' => 'Body Protection Compound-157, PL 14736',
|
||||
];
|
||||
$GLOBALS['pr_test_posts'][2] = (object) [ 'post_title' => 'BPC-157', 'ID' => 2 ];
|
||||
$this->call_private( $this->migration, 'backfill_post', [ 2 ] );
|
||||
$written = null;
|
||||
foreach ( $GLOBALS['pr_test_updated_meta'] as $entry ) {
|
||||
if ( '_pr_aliases' === $entry['key'] ) {
|
||||
$written = $entry['value'];
|
||||
}
|
||||
}
|
||||
$aliases = json_decode( $written, true );
|
||||
$this->assertIsArray( $aliases );
|
||||
$this->assertCount( 2, $aliases );
|
||||
}
|
||||
|
||||
// ── PubChem skip path ─────────────────────────────────────────────.
|
||||
|
||||
public function test_backfill_post_returns_pubchem_skip_when_network_fails(): void {
|
||||
$mock_pubchem = new class extends PR_Core_Pubchem_Client {
|
||||
public function fetch_by_cid( string $cid ): ?array { return null; }
|
||||
public function fetch_by_name( string $name ): ?array { return null; }
|
||||
};
|
||||
$mock_migration = new PR_Core_Migration_0004_Backfill_Peptide_Meta( $mock_pubchem );
|
||||
|
||||
$GLOBALS['pr_test_postmeta'][10] = [];
|
||||
$GLOBALS['pr_test_posts'][10] = (object) [ 'post_title' => 'IGF-1 LR3', 'ID' => 10 ];
|
||||
|
||||
$result = $this->call_private( $mock_migration, 'backfill_post', [ 10 ] );
|
||||
$this->assertSame( 'pubchem_skip', $result );
|
||||
$this->assertEmpty( $GLOBALS['pr_test_updated_meta'] );
|
||||
}
|
||||
}
|
||||
247
tests/unit/PeptideCptTest.php
Normal file
247
tests/unit/PeptideCptTest.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
/**
|
||||
* Unit tests for PR_Core_Peptide_CPT (v0.2.0 CPT consolidation).
|
||||
*
|
||||
* Ported from tests/unit/test-peptide-cpt.php.
|
||||
*
|
||||
* Coverage:
|
||||
* - Constants match the v0.2.0 consolidated contract.
|
||||
* - TAX_FAMILY constant is removed.
|
||||
* - register_peptide_post_type() guard: no-op when post_type_exists.
|
||||
* - register_peptide_post_type() payload: args match contract.
|
||||
* - register_taxonomies() guard: no-op when taxonomy_exists.
|
||||
* - register_taxonomies() only registers peptide_category.
|
||||
* - register_hooks() adds action on 'init'.
|
||||
*
|
||||
* @package PeptideRepoCore\Tests
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for PR_Core_Peptide_CPT.
|
||||
*/
|
||||
class PeptideCptTest extends TestCase {
|
||||
|
||||
/**
|
||||
* Reset global WP state before each test.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['pr_core_test_state'] = [
|
||||
'existing_post_types' => [],
|
||||
'existing_taxonomies' => [],
|
||||
'registered_post_types' => [],
|
||||
'registered_taxonomies' => [],
|
||||
'registered_meta' => [],
|
||||
'added_actions' => [],
|
||||
'added_filters' => [],
|
||||
];
|
||||
// Ensure CPT class is loaded.
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
|
||||
}
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────.
|
||||
|
||||
public function test_post_type_constant(): void {
|
||||
$this->assertSame( 'peptide', PR_Core_Peptide_CPT::POST_TYPE );
|
||||
}
|
||||
|
||||
public function test_tax_category_constant(): void {
|
||||
$this->assertSame( 'peptide_category', PR_Core_Peptide_CPT::TAX_CATEGORY );
|
||||
}
|
||||
|
||||
public function test_capability_constant(): void {
|
||||
$this->assertSame( 'manage_peptide_content', PR_Core_Peptide_CPT::CAPABILITY );
|
||||
}
|
||||
|
||||
public function test_tax_family_constant_removed(): void {
|
||||
$reflection = new ReflectionClass( PR_Core_Peptide_CPT::class );
|
||||
$this->assertFalse(
|
||||
$reflection->hasConstant( 'TAX_FAMILY' ),
|
||||
'TAX_FAMILY constant should have been removed in v0.2.0'
|
||||
);
|
||||
}
|
||||
|
||||
// ── CPT registration guard ────────────────────────────────────────.
|
||||
|
||||
public function test_register_post_type_noop_when_already_exists(): void {
|
||||
$GLOBALS['pr_core_test_state']['existing_post_types'] = [ 'peptide' ];
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$this->assertEmpty(
|
||||
$GLOBALS['pr_core_test_state']['registered_post_types'],
|
||||
'register_peptide_post_type() must no-op when post_type_exists("peptide") is true'
|
||||
);
|
||||
}
|
||||
|
||||
// ── CPT registration payload ──────────────────────────────────────.
|
||||
|
||||
public function test_register_post_type_registers_peptide_slug(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$this->assertArrayHasKey(
|
||||
'peptide',
|
||||
$GLOBALS['pr_core_test_state']['registered_post_types']
|
||||
);
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_public(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertTrue( $args['public'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_publicly_queryable(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertTrue( $args['publicly_queryable'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_show_in_rest(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertTrue( $args['show_in_rest'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_rest_base(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertSame( 'peptides', $args['rest_base'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_no_rest_namespace(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertArrayNotHasKey(
|
||||
'rest_namespace',
|
||||
$args,
|
||||
'rest_namespace must be absent — defaults to wp/v2 for Gutenberg compatibility'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_capability_type(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertSame( 'post', $args['capability_type'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_map_meta_cap(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertTrue( $args['map_meta_cap'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_not_hierarchical(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertFalse( $args['hierarchical'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_has_archive(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertTrue( $args['has_archive'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_rewrite_slug(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertSame( 'peptides', $args['rewrite']['slug'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_post_type_args_rewrite_with_front(): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$this->assertFalse( $args['rewrite']['with_front'] ?? null );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider supports_provider
|
||||
*/
|
||||
public function test_register_post_type_supports_feature( string $feature ): void {
|
||||
PR_Core_Peptide_CPT::register_peptide_post_type();
|
||||
$args = $GLOBALS['pr_core_test_state']['registered_post_types']['peptide'];
|
||||
$supports = $args['supports'] ?? [];
|
||||
$this->assertContains( $feature, $supports );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
public static function supports_provider(): array {
|
||||
return [
|
||||
[ 'title' ],
|
||||
[ 'editor' ],
|
||||
[ 'thumbnail' ],
|
||||
[ 'excerpt' ],
|
||||
[ 'revisions' ],
|
||||
[ 'custom-fields' ],
|
||||
];
|
||||
}
|
||||
|
||||
// ── Taxonomy guard ────────────────────────────────────────────────.
|
||||
|
||||
public function test_register_taxonomies_noop_when_already_exists(): void {
|
||||
$GLOBALS['pr_core_test_state']['existing_taxonomies'] = [ 'peptide_category' ];
|
||||
PR_Core_Peptide_CPT::register_taxonomies();
|
||||
$this->assertEmpty( $GLOBALS['pr_core_test_state']['registered_taxonomies'] );
|
||||
}
|
||||
|
||||
// ── Taxonomy registration payload ─────────────────────────────────.
|
||||
|
||||
public function test_register_taxonomies_registers_peptide_category(): void {
|
||||
PR_Core_Peptide_CPT::register_taxonomies();
|
||||
$this->assertArrayHasKey(
|
||||
'peptide_category',
|
||||
$GLOBALS['pr_core_test_state']['registered_taxonomies']
|
||||
);
|
||||
}
|
||||
|
||||
public function test_register_taxonomies_does_not_register_pr_peptide_family(): void {
|
||||
PR_Core_Peptide_CPT::register_taxonomies();
|
||||
$this->assertArrayNotHasKey(
|
||||
'pr_peptide_family',
|
||||
$GLOBALS['pr_core_test_state']['registered_taxonomies'],
|
||||
'pr_peptide_family taxonomy must not be registered (removed in v0.2.0)'
|
||||
);
|
||||
}
|
||||
|
||||
public function test_register_taxonomies_does_not_register_peptide_family(): void {
|
||||
PR_Core_Peptide_CPT::register_taxonomies();
|
||||
$this->assertArrayNotHasKey(
|
||||
'peptide_family',
|
||||
$GLOBALS['pr_core_test_state']['registered_taxonomies']
|
||||
);
|
||||
}
|
||||
|
||||
public function test_register_taxonomies_attached_to_peptide_cpt(): void {
|
||||
PR_Core_Peptide_CPT::register_taxonomies();
|
||||
$tax = $GLOBALS['pr_core_test_state']['registered_taxonomies']['peptide_category'];
|
||||
$this->assertSame( 'peptide', $tax['object_type'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_taxonomies_is_hierarchical(): void {
|
||||
PR_Core_Peptide_CPT::register_taxonomies();
|
||||
$tax = $GLOBALS['pr_core_test_state']['registered_taxonomies']['peptide_category'];
|
||||
$this->assertTrue( $tax['args']['hierarchical'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_taxonomies_show_in_rest(): void {
|
||||
PR_Core_Peptide_CPT::register_taxonomies();
|
||||
$tax = $GLOBALS['pr_core_test_state']['registered_taxonomies']['peptide_category'];
|
||||
$this->assertTrue( $tax['args']['show_in_rest'] ?? null );
|
||||
}
|
||||
|
||||
public function test_register_taxonomies_rewrite_slug(): void {
|
||||
PR_Core_Peptide_CPT::register_taxonomies();
|
||||
$tax = $GLOBALS['pr_core_test_state']['registered_taxonomies']['peptide_category'];
|
||||
$this->assertSame( 'peptide-category', $tax['args']['rewrite']['slug'] ?? null );
|
||||
}
|
||||
|
||||
// ── Hook registration ─────────────────────────────────────────────.
|
||||
|
||||
public function test_register_hooks_adds_init_action(): void {
|
||||
$cpt = new PR_Core_Peptide_CPT();
|
||||
$cpt->register_hooks();
|
||||
$hooks = array_column( $GLOBALS['pr_core_test_state']['added_actions'], 'hook' );
|
||||
$this->assertContains( 'init', $hooks );
|
||||
}
|
||||
}
|
||||
126
tests/unit/VerificationScannerTest.php
Normal file
126
tests/unit/VerificationScannerTest.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* Unit tests for PR_Core_Verification_Scanner::compute_status().
|
||||
*
|
||||
* Ported from tests/unit/test-verification-scanner.php.
|
||||
*
|
||||
* Coverage:
|
||||
* - Empty last_verified → overdue.
|
||||
* - Days < 90% of threshold → current.
|
||||
* - Days >= 90% and < 100% → due.
|
||||
* - Days >= threshold → overdue.
|
||||
* - Boundary at exactly 90% → due.
|
||||
* - high velocity applies 60-day threshold from option.
|
||||
* - low velocity applies 365-day constant (ignores option).
|
||||
* - medium/default applies 180-day threshold from option.
|
||||
* - Unknown velocity falls to medium/default.
|
||||
*
|
||||
* @package PeptideRepoCore\Tests
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for PR_Core_Verification_Scanner::compute_status().
|
||||
*/
|
||||
class VerificationScannerTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['pr_core_options'] = [];
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/scanner/class-pr-core-verification-scanner.php';
|
||||
}
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────────.
|
||||
|
||||
private function days_ago( int $days ): string {
|
||||
return gmdate( 'Y-m-d', time() - ( $days * DAY_IN_SECONDS ) );
|
||||
}
|
||||
|
||||
// ── Empty last_verified ───────────────────────────────────────────.
|
||||
|
||||
public function test_empty_last_verified_returns_overdue(): void {
|
||||
$this->assertSame(
|
||||
'overdue',
|
||||
PR_Core_Verification_Scanner::compute_status( '', 'medium' )
|
||||
);
|
||||
}
|
||||
|
||||
// ── Medium velocity (default 180-day threshold) ───────────────────.
|
||||
|
||||
public function test_medium_160_days_is_current(): void {
|
||||
$this->assertSame( 'current', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 160 ), 'medium' ) );
|
||||
}
|
||||
|
||||
public function test_medium_163_days_is_due(): void {
|
||||
$this->assertSame( 'due', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 163 ), 'medium' ) );
|
||||
}
|
||||
|
||||
public function test_medium_180_days_is_overdue(): void {
|
||||
$this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 180 ), 'medium' ) );
|
||||
}
|
||||
|
||||
public function test_medium_200_days_is_overdue(): void {
|
||||
$this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 200 ), 'medium' ) );
|
||||
}
|
||||
|
||||
// ── 90% boundary exactness ────────────────────────────────────────.
|
||||
|
||||
public function test_boundary_89_days_threshold_100_is_current(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 100 ];
|
||||
$this->assertSame( 'current', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 89 ), 'medium' ) );
|
||||
}
|
||||
|
||||
public function test_boundary_90_days_threshold_100_is_due(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 100 ];
|
||||
$this->assertSame( 'due', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 90 ), 'medium' ) );
|
||||
}
|
||||
|
||||
public function test_boundary_100_days_threshold_100_is_overdue(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 100 ];
|
||||
$this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 100 ), 'medium' ) );
|
||||
}
|
||||
|
||||
// ── High velocity (60-day threshold from option) ──────────────────.
|
||||
|
||||
public function test_high_50_days_is_current(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_high_velocity_threshold' => 60 ];
|
||||
$this->assertSame( 'current', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 50 ), 'high' ) );
|
||||
}
|
||||
|
||||
public function test_high_55_days_is_due(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_high_velocity_threshold' => 60 ];
|
||||
$this->assertSame( 'due', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 55 ), 'high' ) );
|
||||
}
|
||||
|
||||
public function test_high_61_days_is_overdue(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_high_velocity_threshold' => 60 ];
|
||||
$this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 61 ), 'high' ) );
|
||||
}
|
||||
|
||||
// ── Low velocity (365-day constant — ignores option) ──────────────.
|
||||
|
||||
public function test_low_300_days_is_current(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 999 ];
|
||||
$this->assertSame( 'current', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 300 ), 'low' ) );
|
||||
}
|
||||
|
||||
public function test_low_340_days_is_due(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 999 ];
|
||||
$this->assertSame( 'due', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 340 ), 'low' ) );
|
||||
}
|
||||
|
||||
public function test_low_366_days_is_overdue(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 999 ];
|
||||
$this->assertSame( 'overdue', PR_Core_Verification_Scanner::compute_status( $this->days_ago( 366 ), 'low' ) );
|
||||
}
|
||||
|
||||
// ── Unknown velocity falls to medium/default ──────────────────────.
|
||||
|
||||
public function test_unknown_velocity_uses_default_threshold(): void {
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 180 ];
|
||||
$this->assertSame(
|
||||
'current',
|
||||
PR_Core_Verification_Scanner::compute_status( $this->days_ago( 100 ), 'unknown' )
|
||||
);
|
||||
}
|
||||
}
|
||||
144
tests/unit/VerificationSettingsTest.php
Normal file
144
tests/unit/VerificationSettingsTest.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
/**
|
||||
* Unit tests for PR_Core_Settings verification methods.
|
||||
*
|
||||
* Ported from tests/unit/test-verification-settings.php.
|
||||
*
|
||||
* Coverage:
|
||||
* - sanitize_cadence() accepts valid values, rejects invalid → 'weekly'.
|
||||
* - sanitize_emails() filters invalid addresses, preserves valid ones.
|
||||
* - reschedule_cron() no-ops when old === new value.
|
||||
* - reschedule_cron() clears + reschedules when value changes.
|
||||
*
|
||||
* @package PeptideRepoCore\Tests
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for PR_Core_Settings verification methods.
|
||||
*/
|
||||
class VerificationSettingsTest extends TestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['pr_core_options'] = [];
|
||||
$GLOBALS['pr_core_cron_calls'] = [];
|
||||
$GLOBALS['pr_core_test_state'] = [
|
||||
'existing_post_types' => [],
|
||||
'existing_taxonomies' => [],
|
||||
'registered_post_types' => [],
|
||||
'registered_taxonomies' => [],
|
||||
'registered_meta' => [],
|
||||
'added_actions' => [],
|
||||
'added_filters' => [],
|
||||
];
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/admin/class-pr-core-settings.php';
|
||||
}
|
||||
|
||||
// ── sanitize_cadence() ────────────────────────────────────────────.
|
||||
|
||||
public function test_sanitize_cadence_daily(): void {
|
||||
$this->assertSame( 'daily', PR_Core_Settings::sanitize_cadence( 'daily' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_cadence_weekly(): void {
|
||||
$this->assertSame( 'weekly', PR_Core_Settings::sanitize_cadence( 'weekly' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_cadence_monthly(): void {
|
||||
$this->assertSame( 'monthly', PR_Core_Settings::sanitize_cadence( 'monthly' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_cadence_invalid_defaults_to_weekly(): void {
|
||||
$this->assertSame( 'weekly', PR_Core_Settings::sanitize_cadence( 'hourly' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_cadence_empty_defaults_to_weekly(): void {
|
||||
$this->assertSame( 'weekly', PR_Core_Settings::sanitize_cadence( '' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_cadence_uppercase_defaults_to_weekly(): void {
|
||||
$this->assertSame( 'weekly', PR_Core_Settings::sanitize_cadence( 'DAILY' ) );
|
||||
}
|
||||
|
||||
// ── sanitize_emails() ─────────────────────────────────────────────.
|
||||
|
||||
public function test_sanitize_emails_empty_returns_empty(): void {
|
||||
$this->assertSame( '', PR_Core_Settings::sanitize_emails( '' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_emails_valid_single_kept(): void {
|
||||
$this->assertSame( 'admin@example.com', PR_Core_Settings::sanitize_emails( 'admin@example.com' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_emails_invalid_single_returns_empty(): void {
|
||||
$this->assertSame( '', PR_Core_Settings::sanitize_emails( 'not-an-email' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_emails_valid_pair_both_kept(): void {
|
||||
$this->assertSame( 'a@test.com, b@test.com', PR_Core_Settings::sanitize_emails( 'a@test.com, b@test.com' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_emails_mixed_only_valid_kept(): void {
|
||||
$this->assertSame( 'good@test.com', PR_Core_Settings::sanitize_emails( 'good@test.com, bad-email' ) );
|
||||
}
|
||||
|
||||
public function test_sanitize_emails_whitespace_trimmed(): void {
|
||||
$this->assertSame( 'trimmed@test.com', PR_Core_Settings::sanitize_emails( ' trimmed@test.com ' ) );
|
||||
}
|
||||
|
||||
// ── reschedule_cron(): no-op when same value ──────────────────────.
|
||||
|
||||
public function test_reschedule_cron_noop_when_value_unchanged(): void {
|
||||
PR_Core_Settings::reschedule_cron( 'weekly', 'weekly' );
|
||||
$this->assertEmpty( $GLOBALS['pr_core_cron_calls'] );
|
||||
}
|
||||
|
||||
// ── reschedule_cron(): clears + reschedules on change ─────────────.
|
||||
|
||||
public function test_reschedule_cron_calls_clear_when_changed(): void {
|
||||
PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
|
||||
$actions = array_column( $GLOBALS['pr_core_cron_calls'], 'action' );
|
||||
$this->assertContains( 'clear', $actions );
|
||||
}
|
||||
|
||||
public function test_reschedule_cron_calls_schedule_when_changed(): void {
|
||||
PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
|
||||
$actions = array_column( $GLOBALS['pr_core_cron_calls'], 'action' );
|
||||
$this->assertContains( 'schedule', $actions );
|
||||
}
|
||||
|
||||
public function test_reschedule_cron_schedules_correct_hook(): void {
|
||||
PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
|
||||
$schedule_call = null;
|
||||
foreach ( $GLOBALS['pr_core_cron_calls'] as $call ) {
|
||||
if ( 'schedule' === $call['action'] ) {
|
||||
$schedule_call = $call;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertNotNull( $schedule_call );
|
||||
$this->assertSame( 'pr_core_verification_scan', $schedule_call['hook'] ?? '' );
|
||||
}
|
||||
|
||||
public function test_reschedule_cron_schedules_new_cadence(): void {
|
||||
PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
|
||||
$schedule_call = null;
|
||||
foreach ( $GLOBALS['pr_core_cron_calls'] as $call ) {
|
||||
if ( 'schedule' === $call['action'] ) {
|
||||
$schedule_call = $call;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertSame( 'daily', $schedule_call['recurrence'] ?? '' );
|
||||
}
|
||||
|
||||
public function test_reschedule_cron_clear_fires_before_schedule(): void {
|
||||
PR_Core_Settings::reschedule_cron( 'weekly', 'daily' );
|
||||
$actions = array_column( $GLOBALS['pr_core_cron_calls'], 'action' );
|
||||
$clear_idx = array_search( 'clear', $actions, true );
|
||||
$schedule_idx = array_search( 'schedule', $actions, true );
|
||||
$this->assertLessThan( $schedule_idx, $clear_idx );
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
// ── Additional stubs ─────────────────────────────────────────────────────
|
||||
// ── Additional stubs ─────────────────────────────────────────────────────.
|
||||
|
||||
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
|
||||
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
|
||||
@@ -129,7 +129,7 @@ if ( ! function_exists( 'sanitize_textarea_field' ) ) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stub PR_Core_Peptide_Repository ──────────────────────────────────────
|
||||
// ── Stub PR_Core_Peptide_Repository ──────────────────────────────────────.
|
||||
|
||||
/**
|
||||
* Minimal repository stub — returns whatever is in $GLOBALS['pr_test_peptide_dto'].
|
||||
@@ -140,7 +140,7 @@ class PR_Core_Peptide_Repository {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Load classes under test ───────────────────────────────────────────────
|
||||
// ── Load classes under test ───────────────────────────────────────────────.
|
||||
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-schema-sanitizers.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/cpt/class-pr-core-peptide-cpt.php';
|
||||
@@ -150,7 +150,7 @@ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-faq.ph
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php';
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────────────────
|
||||
// ── Helper ────────────────────────────────────────────────────────────────.
|
||||
|
||||
/**
|
||||
* Create a minimal PR_Core_Peptide_DTO for testing.
|
||||
@@ -184,7 +184,7 @@ function make_peptide_piece_dto( array $overrides = [] ): PR_Core_Peptide_DTO {
|
||||
|
||||
echo "== JSON-LD peptide-piece regression tests (v0.6.2) ==\n\n";
|
||||
|
||||
// ── Setup ─────────────────────────────────────────────────────────────────
|
||||
// ── Setup ─────────────────────────────────────────────────────────────────.
|
||||
|
||||
$GLOBALS['pr_test_the_id'] = 36;
|
||||
$GLOBALS['pr_test_is_singular'] = true;
|
||||
@@ -198,11 +198,11 @@ $GLOBALS['pr_test_peptide_dto'] = make_peptide_piece_dto();
|
||||
|
||||
$jsonld = new PR_Core_Jsonld();
|
||||
|
||||
// ── Regression: inject_graph_pieces fatal reproduced against old contract ─
|
||||
// ── Regression: inject_graph_pieces fatal reproduced against old contract ─.
|
||||
|
||||
echo "Regression: inject_graph_pieces (OLD hook) with plain array fatal:\n";
|
||||
|
||||
// Simulate what Yoast's filter_graph_pieces_to_generate does:
|
||||
// Simulate what Yoast's filter_graph_pieces_to_generate does:.
|
||||
// it calls get_class($piece) on every item. A plain array causes a fatal.
|
||||
$drug_builder = new PR_Core_Jsonld_Drug();
|
||||
$drug_array = $drug_builder->build( $GLOBALS['pr_test_peptide_dto'] );
|
||||
@@ -210,8 +210,8 @@ $drug_array = $drug_builder->build( $GLOBALS['pr_test_peptide_dto'] );
|
||||
$caught_fatal = false;
|
||||
try {
|
||||
// This is the exact operation Yoast does in filter_graph_pieces_to_generate().
|
||||
// Against the old v0.6.1 code (inject via wpseo_schema_graph_pieces), this would
|
||||
// produce: "get_class(): Argument #1 ($object) must be of type object, array given"
|
||||
// Against the old v0.6.1 code (inject via wpseo_schema_graph_pieces), this would.
|
||||
// produce: "get_class(): Argument #1 ($object) must be of type object, array given".
|
||||
$class_result = get_class( $drug_array );
|
||||
} catch ( TypeError $e ) {
|
||||
$caught_fatal = true;
|
||||
@@ -220,7 +220,7 @@ try {
|
||||
}
|
||||
pr_assert( $caught_fatal, 'get_class(array) throws TypeError/Error — confirms the v0.6.1 fatal' );
|
||||
|
||||
// ── Fix: inject_graph_nodes (NEW hook) works without fatal ────────────────
|
||||
// ── Fix: inject_graph_nodes (NEW hook) works without fatal ────────────────.
|
||||
|
||||
echo "\nFix: inject_graph_nodes via wpseo_schema_graph (plain array graph):\n";
|
||||
|
||||
@@ -279,7 +279,7 @@ pr_assert( count( $webpage_nodes ) === 1, 'MedicalWebPage node preserved (no dup
|
||||
$breadcrumb_nodes = array_filter( $result_graph, static fn( $n ) => ( $n['@type'] ?? '' ) === 'BreadcrumbList' );
|
||||
pr_assert( count( $breadcrumb_nodes ) === 1, 'BreadcrumbList node preserved (no duplicate)' );
|
||||
|
||||
// ── FAQ: present when _pr_faq_items has items ─────────────────────────────
|
||||
// ── FAQ: present when _pr_faq_items has items ─────────────────────────────.
|
||||
|
||||
echo "\nFAQ injection via inject_graph_nodes:\n";
|
||||
|
||||
@@ -301,7 +301,7 @@ pr_assert_equals( 'https://peptiderepo.com/peptides/bpc-157/#faq', $faq_node['@i
|
||||
pr_assert( isset( $faq_node['mainEntity'] ), 'FAQPage has mainEntity' );
|
||||
pr_assert_equals( 2, count( $faq_node['mainEntity'] ), 'FAQPage has 2 questions' );
|
||||
|
||||
// ── FAQ: absent when _pr_faq_items empty ─────────────────────────────────
|
||||
// ── FAQ: absent when _pr_faq_items empty ─────────────────────────────────.
|
||||
|
||||
$GLOBALS['pr_test_postmeta'][36]['_pr_faq_items'] = '[]';
|
||||
|
||||
@@ -312,7 +312,7 @@ $faq_nodes_empty = array_filter(
|
||||
);
|
||||
pr_assert( count( $faq_nodes_empty ) === 0, 'FAQPage NOT injected when _pr_faq_items is empty array' );
|
||||
|
||||
// ── Non-peptide page: no injection ───────────────────────────────────────
|
||||
// ── Non-peptide page: no injection ───────────────────────────────────────.
|
||||
|
||||
echo "\nNon-peptide page: no injection:\n";
|
||||
|
||||
@@ -332,7 +332,7 @@ pr_assert( count( $result_non_peptide ) === 1, 'inject_graph_nodes no-ops on non
|
||||
$drug_nodes_np = array_filter( $result_non_peptide, static fn( $n ) => ( is_array( $n['@type'] ?? null ) ? in_array( 'Drug', $n['@type'], true ) : ( $n['@type'] ?? '' ) === 'Drug' ) );
|
||||
pr_assert( count( $drug_nodes_np ) === 0, 'Drug NOT injected on non-peptide page' );
|
||||
|
||||
// ── register_hooks: does NOT hook wpseo_schema_graph_pieces ──────────────
|
||||
// ── register_hooks: does NOT hook wpseo_schema_graph_pieces ──────────────.
|
||||
|
||||
echo "\nregister_hooks: wpseo_schema_graph_pieces must NOT be hooked:\n";
|
||||
|
||||
@@ -341,11 +341,11 @@ $GLOBALS['pr_core_test_state']['added_filters'] = [];
|
||||
$filters_added = [];
|
||||
|
||||
// Override add_filter in the bootstrap to capture calls.
|
||||
// (bootstrap only stubs add_action; we check via a fresh parse of register_hooks.)
|
||||
// (bootstrap only stubs add_action; we check via a fresh parse of register_hooks.).
|
||||
$jsonld2 = new PR_Core_Jsonld();
|
||||
|
||||
// Use Reflection to inspect what hooks register_hooks would call.
|
||||
// Instead: inspect the class source for the string — if inject_graph_pieces is
|
||||
// Instead: inspect the class source for the string — if inject_graph_pieces is.
|
||||
// still registered on wpseo_schema_graph_pieces, the bug is NOT fixed.
|
||||
$src = file_get_contents( PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php' );
|
||||
pr_assert(
|
||||
@@ -361,6 +361,6 @@ pr_assert(
|
||||
'class-pr-core-jsonld.php hooks wpseo_schema_graph for injection'
|
||||
);
|
||||
|
||||
// ── Summary ───────────────────────────────────────────────────────────────
|
||||
// ── Summary ───────────────────────────────────────────────────────────────.
|
||||
|
||||
exit( pr_test_summary() );
|
||||
|
||||
@@ -21,7 +21,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
// ── Additional stubs ─────────────────────────────────────────────────────
|
||||
// ── Additional stubs ─────────────────────────────────────────────────────.
|
||||
|
||||
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
|
||||
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
|
||||
@@ -81,7 +81,7 @@ echo "== JSON-LD WebPage v0.6.1 regression tests ==\n\n";
|
||||
|
||||
$enricher = new PR_Core_Jsonld_Webpage();
|
||||
|
||||
// ── retype_to_medical_webpage: array input, non-peptide singular ──────────
|
||||
// ── retype_to_medical_webpage: array input, non-peptide singular ──────────.
|
||||
|
||||
echo "retype_to_medical_webpage — array input:\n";
|
||||
|
||||
@@ -106,7 +106,7 @@ $GLOBALS['pr_test_singular_type'] = '';
|
||||
$result = $enricher->retype_to_medical_webpage( 'CollectionPage' );
|
||||
pr_assert_equals( 'CollectionPage', $result, 'non-singular string: passes through unchanged' );
|
||||
|
||||
// ── enrich_webpage_piece: @type as array injects enrichments ─────────────
|
||||
// ── enrich_webpage_piece: @type as array injects enrichments ─────────────.
|
||||
|
||||
echo "\nenrich_webpage_piece — @type as array:\n";
|
||||
|
||||
@@ -160,6 +160,6 @@ $graph_non_peptide = [
|
||||
$result_non_peptide = $enricher->enrich_webpage_piece( $graph_non_peptide, new stdClass() );
|
||||
pr_assert( ! isset( $result_non_peptide[0]['lastReviewed'] ), 'non-peptide: lastReviewed NOT injected' );
|
||||
|
||||
// ── Summary ──────────────────────────────────────────────────────────────
|
||||
// ── Summary ──────────────────────────────────────────────────────────────.
|
||||
|
||||
exit( pr_test_summary() );
|
||||
|
||||
@@ -29,7 +29,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
// ── Additional stubs ─────────────────────────────────────────────────────
|
||||
// ── Additional stubs ─────────────────────────────────────────────────────.
|
||||
|
||||
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
|
||||
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
|
||||
@@ -111,7 +111,7 @@ require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-faq.ph
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld-webpage.php';
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/frontend/class-pr-core-jsonld.php';
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────.
|
||||
|
||||
/**
|
||||
* Create a minimal PR_Core_Peptide_DTO for testing.
|
||||
@@ -145,7 +145,7 @@ function make_peptide_dto( array $overrides = [] ): PR_Core_Peptide_DTO {
|
||||
|
||||
echo "== JSON-LD v0.6.0 unit tests ==\n\n";
|
||||
|
||||
// ── Drug @id ─────────────────────────────────────────────────────────────
|
||||
// ── Drug @id ─────────────────────────────────────────────────────────────.
|
||||
|
||||
echo "Drug @id:\n";
|
||||
|
||||
@@ -160,14 +160,14 @@ pr_assert_equals(
|
||||
'Drug @id = {permalink}#drug'
|
||||
);
|
||||
|
||||
// ── Drug @type ───────────────────────────────────────────────────────────
|
||||
// ── Drug @type ───────────────────────────────────────────────────────────.
|
||||
|
||||
echo "\nDrug @type:\n";
|
||||
pr_assert( is_array( $drug_node['@type'] ), 'Drug @type is an array' );
|
||||
pr_assert( in_array( 'Drug', $drug_node['@type'], true ), '@type includes "Drug"' );
|
||||
pr_assert( in_array( 'MolecularEntity', $drug_node['@type'], true ), '@type includes "MolecularEntity"' );
|
||||
|
||||
// ── Fields present when meta set ─────────────────────────────────────────
|
||||
// ── Fields present when meta set ─────────────────────────────────────────.
|
||||
|
||||
echo "\nFields present when _pr_* meta set:\n";
|
||||
|
||||
@@ -190,7 +190,7 @@ pr_assert_equals( 'g/mol', $drug_node_populated['molecularWeight']['unitText'],
|
||||
pr_assert( isset( $drug_node_populated['alternateName'] ), 'alternateName present when _pr_aliases set' );
|
||||
pr_assert_equals( 2, count( $drug_node_populated['alternateName'] ), 'alternateName has correct count' );
|
||||
|
||||
// ── Fields omitted when meta empty ───────────────────────────────────────
|
||||
// ── Fields omitted when meta empty ───────────────────────────────────────.
|
||||
|
||||
echo "\nFields OMITTED when meta empty:\n";
|
||||
|
||||
@@ -201,7 +201,7 @@ pr_assert( ! isset( $drug_node_empty['molecularFormula'] ), 'molecularFormula OM
|
||||
pr_assert( ! isset( $drug_node_empty['molecularWeight'] ), 'molecularWeight OMITTED when meta empty' );
|
||||
pr_assert( ! isset( $drug_node_empty['alternateName'] ), 'alternateName OMITTED when _pr_aliases empty' );
|
||||
|
||||
// ── FAQPage present when _pr_faq_items has items ─────────────────────────
|
||||
// ── FAQPage present when _pr_faq_items has items ─────────────────────────.
|
||||
|
||||
echo "\nFAQPage emission:\n";
|
||||
|
||||
@@ -224,7 +224,7 @@ pr_assert_equals( 2, count( $faq_piece['mainEntity'] ), 'FAQPage has 2 questions
|
||||
pr_assert_equals( 'Question', $faq_piece['mainEntity'][0]['@type'], 'Question @type correct' );
|
||||
pr_assert( isset( $faq_piece['mainEntity'][0]['acceptedAnswer'] ), 'Question has acceptedAnswer' );
|
||||
|
||||
// ── FAQPage absent when _pr_faq_items empty ──────────────────────────────
|
||||
// ── FAQPage absent when _pr_faq_items empty ──────────────────────────────.
|
||||
|
||||
$GLOBALS['pr_test_postmeta'][1] = [ '_pr_faq_items' => '[]' ];
|
||||
$faq_empty = $faq_builder->build( 1, $permalink );
|
||||
@@ -234,7 +234,7 @@ $GLOBALS['pr_test_postmeta'][1] = [];
|
||||
$faq_absent = $faq_builder->build( 1, $permalink );
|
||||
pr_assert( null === $faq_absent, 'FAQPage piece IS null when _pr_faq_items absent' );
|
||||
|
||||
// ── MedicalWebPage retype ────────────────────────────────────────────────
|
||||
// ── MedicalWebPage retype ────────────────────────────────────────────────.
|
||||
|
||||
echo "\nMedicalWebPage retype:\n";
|
||||
|
||||
@@ -250,7 +250,7 @@ $GLOBALS['pr_test_singular_type'] = '';
|
||||
$result = $webpage_enricher->retype_to_medical_webpage( 'WebPage' );
|
||||
pr_assert_equals( 'WebPage', $result, 'passes through unchanged on non-peptide pages' );
|
||||
|
||||
// ── No duplicate WebPage / BreadcrumbList nodes ───────────────────────────
|
||||
// ── No duplicate WebPage / BreadcrumbList nodes ───────────────────────────.
|
||||
|
||||
echo "\nNo duplicate WebPage/BreadcrumbList nodes:\n";
|
||||
|
||||
@@ -270,7 +270,7 @@ pr_assert( ! in_array( 'BreadcrumbList', $types_from_builders, true ), 'Breadcru
|
||||
pr_assert( in_array( 'Drug', $types_from_builders, true ), 'Drug IS in our emitted pieces' );
|
||||
pr_assert( in_array( 'FAQPage', $types_from_builders, true ), 'FAQPage IS in our emitted pieces when items present' );
|
||||
|
||||
// ── PR_Core_Schema_Sanitizers::sanitize_faq_items() ──────────────────────
|
||||
// ── PR_Core_Schema_Sanitizers::sanitize_faq_items() ──────────────────────.
|
||||
|
||||
echo "\nPR_Core_Schema_Sanitizers::sanitize_faq_items():\n";
|
||||
|
||||
@@ -294,6 +294,6 @@ $result_incomplete = PR_Core_Schema_Sanitizers::sanitize_faq_items( $incomplete_
|
||||
$decoded_incomplete = json_decode( $result_incomplete, true );
|
||||
pr_assert_equals( 0, count( $decoded_incomplete ), 'item without answer field dropped' );
|
||||
|
||||
// ── Summary ──────────────────────────────────────────────────────────────
|
||||
// ── Summary ──────────────────────────────────────────────────────────────.
|
||||
|
||||
exit( pr_test_summary() );
|
||||
|
||||
@@ -27,7 +27,7 @@ declare(strict_types=1);
|
||||
// Bootstrap the lightweight WP stubs.
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
// ── Additional stubs needed for migration 0004 ──────────────────────────
|
||||
// ── Additional stubs needed for migration 0004 ──────────────────────────.
|
||||
|
||||
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
|
||||
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
|
||||
@@ -85,7 +85,7 @@ require_once PR_CORE_PLUGIN_DIR . 'includes/migrations/class-pr-core-pubchem-cli
|
||||
// Load migration class under test.
|
||||
require_once PR_CORE_PLUGIN_DIR . 'includes/migrations/class-pr-core-migration-0004-backfill-peptide-meta.php';
|
||||
|
||||
// ── Expose private methods via reflection ────────────────────────────────
|
||||
// ── Expose private methods via reflection ────────────────────────────────.
|
||||
|
||||
/**
|
||||
* Call a private/protected method on an object via reflection.
|
||||
@@ -105,7 +105,7 @@ $migration = new PR_Core_Migration_0004_Backfill_Peptide_Meta();
|
||||
|
||||
echo "== PR_Core_Migration_0004 unit tests ==\n\n";
|
||||
|
||||
// ── parse_weight() ───────────────────────────────────────────────────────
|
||||
// ── parse_weight() ───────────────────────────────────────────────────────.
|
||||
|
||||
echo "parse_weight():\n";
|
||||
|
||||
@@ -145,7 +145,7 @@ pr_assert_equals(
|
||||
'strips whitespace and lowercase "da"'
|
||||
);
|
||||
|
||||
// ── parse_aliases_string() ───────────────────────────────────────────────
|
||||
// ── parse_aliases_string() ───────────────────────────────────────────────.
|
||||
|
||||
echo "\nparse_aliases_string():\n";
|
||||
|
||||
@@ -167,8 +167,8 @@ pr_assert_equals( '[]', $whitespace_result, 'whitespace-only segments dropped, r
|
||||
$single = json_decode( $migration->parse_aliases_string( 'Only One' ), true );
|
||||
pr_assert_equals( 1, count( $single ), 'single alias produces array of 1' );
|
||||
|
||||
// ── sanitize_formula() ───────────────────────────────────────────────────
|
||||
// Five required cases (Option B — validate, don't allowlist):
|
||||
// ── sanitize_formula() ───────────────────────────────────────────────────.
|
||||
// Five required cases (Option B — validate, don't allowlist):.
|
||||
// 1. Valid formula unchanged.
|
||||
// 2. Parenthesised formula preserved.
|
||||
// 3. Newline-injected payload — "injection" stripped, formula kept.
|
||||
@@ -219,7 +219,7 @@ pr_assert_equals(
|
||||
'whitespace-only returns empty string'
|
||||
);
|
||||
|
||||
// ── Idempotency: skips when all three PR Core keys populated ─────────────
|
||||
// ── Idempotency: skips when all three PR Core keys populated ─────────────.
|
||||
|
||||
echo "\nIdempotency (all _pr_* keys populated):\n";
|
||||
|
||||
@@ -235,7 +235,7 @@ $result = call_private( $migration, 'backfill_post', [ 1 ] );
|
||||
pr_assert_equals( 'skipped', $result, 'returns "skipped" when all three keys non-empty' );
|
||||
pr_assert( empty( $GLOBALS['pr_test_updated_meta'] ), 'no update_post_meta calls when skipped' );
|
||||
|
||||
// ── PSA source path ──────────────────────────────────────────────────────
|
||||
// ── PSA source path ──────────────────────────────────────────────────────.
|
||||
|
||||
echo "\nPSA source path:\n";
|
||||
|
||||
@@ -277,7 +277,7 @@ pr_assert( $formula_written, 'molecular formula meta was written' );
|
||||
pr_assert( $weight_written, 'molecular weight meta was written' );
|
||||
pr_assert( $aliases_written, 'aliases meta was written' );
|
||||
|
||||
// ── Partial re-run: existing values preserved ────────────────────────────
|
||||
// ── Partial re-run: existing values preserved ────────────────────────────.
|
||||
|
||||
echo "\nIdempotency: only some keys populated (partial re-run):\n";
|
||||
|
||||
@@ -297,7 +297,7 @@ $GLOBALS['pr_test_posts'][3] = (object) [ 'post_title' => 'BPC-157', 'ID' => 3 ]
|
||||
$result = call_private( $migration, 'backfill_post', [ 3 ] );
|
||||
pr_assert_equals( 'copied_psa', $result, 'runs PSA copy even when only some keys populated' );
|
||||
|
||||
// ── PubChem skip path: no PSA data, no network ──────────────────────────
|
||||
// ── PubChem skip path: no PSA data, no network ──────────────────────────.
|
||||
|
||||
echo "\nPubChem skip path (no PSA data, no network in unit tests):\n";
|
||||
|
||||
@@ -317,6 +317,6 @@ $result = call_private( $mock_migration, 'backfill_post', [ 10 ] );
|
||||
pr_assert_equals( 'pubchem_skip', $result, 'returns "pubchem_skip" when PubChem network fails' );
|
||||
pr_assert( empty( $GLOBALS['pr_test_updated_meta'] ), 'no meta written on pubchem_skip' );
|
||||
|
||||
// ── Summary ──────────────────────────────────────────────────────────────
|
||||
// ── Summary ──────────────────────────────────────────────────────────────.
|
||||
|
||||
exit( pr_test_summary() );
|
||||
|
||||
@@ -22,7 +22,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
// ── Additional stubs needed by the scanner ──────────────────────────────
|
||||
// ── Additional stubs needed by the scanner ──────────────────────────────.
|
||||
|
||||
if ( ! defined( 'DAY_IN_SECONDS' ) ) {
|
||||
define( 'DAY_IN_SECONDS', 86400 );
|
||||
@@ -37,11 +37,11 @@ if ( ! function_exists( 'get_option' ) ) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Load the scanner class ──────────────────────────────────────────────
|
||||
// ── Load the scanner class ──────────────────────────────────────────────.
|
||||
|
||||
require_once __DIR__ . '/../../includes/scanner/class-pr-core-verification-scanner.php';
|
||||
|
||||
// ── Helper: build a date N days ago ────────────────────────────────────
|
||||
// ── Helper: build a date N days ago ────────────────────────────────────.
|
||||
|
||||
function days_ago( int $days ): string {
|
||||
return gmdate( 'Y-m-d', time() - ( $days * DAY_IN_SECONDS ) );
|
||||
@@ -49,7 +49,7 @@ function days_ago( int $days ): string {
|
||||
|
||||
echo "== PR_Core_Verification_Scanner::compute_status() unit tests ==\n\n";
|
||||
|
||||
// ── Empty last_verified ─────────────────────────────────────────────────
|
||||
// ── Empty last_verified ─────────────────────────────────────────────────.
|
||||
|
||||
echo "Empty last_verified:\n";
|
||||
pr_assert_equals(
|
||||
@@ -58,7 +58,7 @@ pr_assert_equals(
|
||||
'empty string → overdue'
|
||||
);
|
||||
|
||||
// ── Medium velocity (default 180-day threshold) ─────────────────────────
|
||||
// ── Medium velocity (default 180-day threshold) ─────────────────────────.
|
||||
|
||||
echo "\nMedium velocity (threshold=180, option default):\n";
|
||||
$GLOBALS['pr_core_options'] = [];
|
||||
@@ -75,7 +75,7 @@ pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_
|
||||
// 200 days ago → overdue.
|
||||
pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_ago( 200 ), 'medium' ), '200 days → overdue' );
|
||||
|
||||
// ── 90% boundary exactness ──────────────────────────────────────────────
|
||||
// ── 90% boundary exactness ──────────────────────────────────────────────.
|
||||
|
||||
echo "\n90% boundary (threshold=100):\n";
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 100 ];
|
||||
@@ -89,7 +89,7 @@ pr_assert_equals( 'due', PR_Core_Verification_Scanner::compute_status( days_ago(
|
||||
// 100 days → 100 >= 100 → overdue.
|
||||
pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_ago( 100 ), 'medium' ), '100 days (threshold 100) → overdue' );
|
||||
|
||||
// ── High velocity (60-day threshold from option) ────────────────────────
|
||||
// ── High velocity (60-day threshold from option) ────────────────────────.
|
||||
|
||||
echo "\nHigh velocity (threshold=60 from option):\n";
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_high_velocity_threshold' => 60 ];
|
||||
@@ -103,7 +103,7 @@ pr_assert_equals( 'due', PR_Core_Verification_Scanner::compute_status( days_ago(
|
||||
// 61 days → overdue.
|
||||
pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_ago( 61 ), 'high' ), 'high: 61 days → overdue' );
|
||||
|
||||
// ── Low velocity (365-day constant — ignores option) ────────────────────
|
||||
// ── Low velocity (365-day constant — ignores option) ────────────────────.
|
||||
|
||||
echo "\nLow velocity (threshold=365, constant, ignores option):\n";
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 999 ]; // Should be ignored for low.
|
||||
@@ -117,12 +117,12 @@ pr_assert_equals( 'due', PR_Core_Verification_Scanner::compute_status( days_ago(
|
||||
// 366 days → overdue.
|
||||
pr_assert_equals( 'overdue', PR_Core_Verification_Scanner::compute_status( days_ago( 366 ), 'low' ), 'low: 366 days → overdue' );
|
||||
|
||||
// ── Unknown velocity falls through to medium/default ────────────────────
|
||||
// ── Unknown velocity falls through to medium/default ────────────────────.
|
||||
|
||||
echo "\nUnknown velocity (falls to default/medium):\n";
|
||||
$GLOBALS['pr_core_options'] = [ 'pr_core_default_threshold' => 180 ];
|
||||
pr_assert_equals( 'current', PR_Core_Verification_Scanner::compute_status( days_ago( 100 ), 'unknown' ), 'unknown velocity: 100 days → current (uses default 180)' );
|
||||
|
||||
// ── Summary ─────────────────────────────────────────────────────────────
|
||||
// ── Summary ─────────────────────────────────────────────────────────────.
|
||||
|
||||
exit( pr_test_summary() );
|
||||
|
||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
// ── Additional stubs needed by the settings class ───────────────────────
|
||||
// ── Additional stubs needed by the settings class ───────────────────────.
|
||||
|
||||
$GLOBALS['pr_core_options'] = [];
|
||||
$GLOBALS['pr_core_cron_calls'] = [];
|
||||
@@ -66,14 +66,14 @@ if ( ! function_exists( 'wp_schedule_event' ) ) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Load the settings class ─────────────────────────────────────────────
|
||||
// ── Load the settings class ─────────────────────────────────────────────.
|
||||
|
||||
require_once __DIR__ . '/../../includes/cpt/class-pr-core-peptide-cpt.php';
|
||||
require_once __DIR__ . '/../../includes/admin/class-pr-core-settings.php';
|
||||
|
||||
echo "== PR_Core_Settings verification methods unit tests ==\n\n";
|
||||
|
||||
// ── sanitize_cadence() ──────────────────────────────────────────────────
|
||||
// ── sanitize_cadence() ──────────────────────────────────────────────────.
|
||||
|
||||
echo "sanitize_cadence():\n";
|
||||
pr_assert_equals( 'daily', PR_Core_Settings::sanitize_cadence( 'daily' ), 'daily → daily' );
|
||||
@@ -83,7 +83,7 @@ pr_assert_equals( 'weekly', PR_Core_Settings::sanitize_cadence( 'hourly' ), 'h
|
||||
pr_assert_equals( 'weekly', PR_Core_Settings::sanitize_cadence( '' ), 'empty → weekly default' );
|
||||
pr_assert_equals( 'weekly', PR_Core_Settings::sanitize_cadence( 'DAILY' ), 'uppercase → weekly (case-sensitive)' );
|
||||
|
||||
// ── sanitize_emails() ───────────────────────────────────────────────────
|
||||
// ── sanitize_emails() ───────────────────────────────────────────────────.
|
||||
|
||||
echo "\nsanitize_emails():\n";
|
||||
pr_assert_equals( '', PR_Core_Settings::sanitize_emails( '' ), 'empty → empty' );
|
||||
@@ -93,14 +93,14 @@ pr_assert_equals( 'a@test.com, b@test.com', PR_Core_Settings::sanitize_emails( '
|
||||
pr_assert_equals( 'good@test.com', PR_Core_Settings::sanitize_emails( 'good@test.com, bad-email' ), 'mixed → only valid kept' );
|
||||
pr_assert_equals( 'trimmed@test.com', PR_Core_Settings::sanitize_emails( ' trimmed@test.com ' ), 'whitespace trimmed around valid email' );
|
||||
|
||||
// ── reschedule_cron(): no-op when same value ────────────────────────────
|
||||
// ── reschedule_cron(): no-op when same value ────────────────────────────.
|
||||
|
||||
echo "\nreschedule_cron() — no-op when value unchanged:\n";
|
||||
$GLOBALS['pr_core_cron_calls'] = [];
|
||||
PR_Core_Settings::reschedule_cron( 'weekly', 'weekly' );
|
||||
pr_assert( empty( $GLOBALS['pr_core_cron_calls'] ), 'no cron calls when old === new' );
|
||||
|
||||
// ── reschedule_cron(): clears + reschedules on change ───────────────────
|
||||
// ── reschedule_cron(): clears + reschedules on change ───────────────────.
|
||||
|
||||
echo "\nreschedule_cron() — reschedules when value changes:\n";
|
||||
$GLOBALS['pr_core_cron_calls'] = [];
|
||||
@@ -126,6 +126,6 @@ $clear_idx = array_search( 'clear', $actions, true );
|
||||
$schedule_idx = array_search( 'schedule', $actions, true );
|
||||
pr_assert( $clear_idx < $schedule_idx, 'clear fires before schedule' );
|
||||
|
||||
// ── Summary ─────────────────────────────────────────────────────────────
|
||||
// ── Summary ─────────────────────────────────────────────────────────────.
|
||||
|
||||
exit( pr_test_summary() );
|
||||
|
||||
Reference in New Issue
Block a user