Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ composer.phar
composer.lock

# PHPUnit
/phpunit.xml
/phpunit.xml.dist
/.phpunit.result.cache
/coverage/
**/.phpunit.result.cache
**/tests/coverage/
**/tests/results/

# PHP CS Fixer
.php_cs.cache
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ Individual plugin versions are tracked separately in their respective plugin hea

### Added
- **community-listings:** Admin meta box ("Provider Listings") on the `community` post type editor, city-level posts only — lets editors view/update the `provider_listings` JSON without CLI access. JSON-safe save via `$wpdb->update()`/`insert()` (avoids `update_post_meta()`'s `wp_unslash()` corrupting `\"` escapes), with byte/provider counts, client-side validation, and post-meta cache invalidation after the direct write (#2)
- Real PHPUnit test suites (unit + WPGraphQL integration) for all three sub-plugins — 148 tests total (community-listings 38, contentful-tables 92, graphql-shortcode-support 18), none of which had any automated tests before. Each sub-plugin gets its own `phpunit.xml`/`tests/Unit`/`tests/Integration`, extending `silverassist/wp-plugin-kernel`'s `Testing\TestCase` (already available via the existing runtime dependency). Two new repo-root scripts (`scripts/install-wp-tests.sh`, a thin wrapper around the vendored `silverassist/wp-coding-standards` installer; `scripts/install-wpgraphql-for-tests.sh`, adapted from `silver-assist-security`) set up one shared WP test environment for all three sub-plugins to run against

### Changed
- Adopted the `SilverAssistWP` PHPCS ruleset and composed shared `silverassist/coding-standards` + `silverassist/wp-coding-standards` PHPStan base configs across all three sub-plugins, replacing hand-rolled rulesets and duplicated static-analysis baselines (#3)
- **community-listings, contentful-tables:** `phpcs.xml` now excludes `tests/*`, matching `graphql-shortcode-support`'s existing convention — test files intentionally aren't held to the same doc-comment-per-method WPCS rules as plugin source

### Fixed
- **community-listings:** Resolved all 3 pre-existing PHPStan errors — dead-code truthy check in `SettingsPage::render_settings_page()`, a `register_graphql_field()` call missing its own `function_exists()` guard, and an unreachable array-offset fallback in the GraphQL type-mapping refactored to a `match` expression
- **contentful-tables:** `TableDataLoader::load_tables_from_database()` selected a non-existent `table_id` column — the table created by `Activator` has always used `entry_id`. Writing the new database-fallback test surfaced this immediately; the fallback path itself was never previously reachable by any test, so this SQL error would have surfaced only in production the first time a site had no matching table files or post meta
- **Makefile:** `phpcs`, `phpstan`, `install`, `install-dev`, and `build` targets no longer silently ignore failures in any sub-plugin but the last one in `$(PLUGINS)` — each `for` loop now propagates a non-zero exit status, so CI actually fails when any sub-plugin has a real violation (`phpcs`/`phpstan` previously didn't: the 3 PHPStan errors above went undetected by CI since this repo's first PHPStan adoption)
- **Makefile:** `phpcs` target now calls `vendor/bin/phpcs --warning-severity=0` directly instead of `composer run phpcs`, matching exactly what CI's own PHPCS step runs — previously `make phpcs` treated pre-existing warnings (discouraged `json_encode()`/`file_get_contents()` calls, a missing nonce-verification annotation) as failures that CI itself was configured to ignore, so the two disagreed on what counted as passing

Expand Down
10 changes: 9 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,20 @@ final class YourComponent implements LoadableInterface {
- **PHPCS**: Code standards compliance
- **PHPStan**: Static analysis
- **Composer validation**: Dependency management
- **PHPUnit**: Unit + WPGraphQL integration tests (per sub-plugin, `tests/Unit` and `tests/Integration`)

Run all tests:
Run linting/static analysis for all sub-plugins:
```bash
make test
```

Run a sub-plugin's PHPUnit suite (requires a WordPress test environment — set up once, shared by all three sub-plugins):
```bash
bash scripts/install-wp-tests.sh wordpress_test root '' localhost latest
bash scripts/install-wpgraphql-for-tests.sh
cd community-listings && WP_TESTS_DIR=/tmp/wordpress-tests-lib composer run phpunit
```

## 📤 Pull Request Process

1. **Update documentation** if needed
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ make phpstan # Static analysis (Level 8)
make lint # Run PHPCS + PHPStan together
```

### Automated Tests

Each sub-plugin has its own PHPUnit suite (`tests/Unit`, `tests/Integration`) covering business logic and
WPGraphQL registration behavior. They share one WordPress test environment, set up once:

```bash
bash scripts/install-wp-tests.sh wordpress_test root '' localhost latest
bash scripts/install-wpgraphql-for-tests.sh

cd community-listings && WP_TESTS_DIR=/tmp/wordpress-tests-lib composer run phpunit
```

## 🤝 Contributing

1. Fork the repository
Expand Down
8 changes: 8 additions & 0 deletions community-listings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,22 @@ community-listings/
├── composer.json # Composer config with PSR-4 autoload
├── phpcs.xml # WPCS configuration
├── phpstan.neon # PHPStan Level 8 configuration
├── phpunit.xml # PHPUnit configuration
├── includes/
│ ├── Admin/
│ │ ├── ProviderListingsMetaBox.php # Provider Listings meta box (priority 25)
│ │ └── SettingsPage.php # Admin settings UI (priority 30)
│ ├── Core/
│ │ ├── Activator.php # Activation/deactivation handlers
│ │ └── Plugin.php # Singleton bootstrap (extends wp-plugin-kernel's AbstractPlugin)
│ └── Service/
│ ├── CptRegistrar.php # CPT + meta field registration (priority 10)
│ ├── GraphQLResolver.php # WPGraphQL do_shortcode() (priority 20)
│ └── RestApiFilters.php # REST API query filters (priority 20)
└── tests/
├── bootstrap.php # PHPUnit bootstrap (WP test suite + WPGraphQL)
├── Unit/ # Business-logic tests
└── Integration/ # WPGraphQL registration tests
```

### Component Loading Order
Expand Down
19 changes: 17 additions & 2 deletions community-listings/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,25 @@
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"php-stubs/wordpress-tests-stubs": "^6.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^9.6",
"silverassist/coding-standards": "^1.0",
"silverassist/wp-coding-standards": "^1.0",
"szepeviktor/phpstan-wordpress": "^2.0",
"wp-coding-standards/wpcs": "^3.1"
"wp-coding-standards/wpcs": "^3.1",
"yoast/phpunit-polyfills": "^4.0"
},
"autoload": {
"psr-4": {
"SilverAssist\\CommunityListings\\": "includes/"
}
},
"autoload-dev": {
"psr-4": {
"SilverAssist\\CommunityListings\\Tests\\": "tests/"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
Expand All @@ -32,6 +40,13 @@
},
"scripts": {
"phpcs": "phpcs",
"phpstan": "phpstan analyse --memory-limit=512M"
"phpcbf": "phpcbf",
"phpstan": "phpstan analyse --memory-limit=512M",
"phpunit": "phpunit",
"test": [
"@phpcs",
"@phpstan",
"@phpunit"
]
}
}
1 change: 1 addition & 0 deletions community-listings/phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<file>.</file>
<exclude-pattern>vendor/*</exclude-pattern>
<exclude-pattern>node_modules/*</exclude-pattern>
<exclude-pattern>tests/*</exclude-pattern>

<arg name="extensions" value="php"/>
<arg name="colors"/>
Expand Down
42 changes: 42 additions & 0 deletions community-listings/phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
verbose="true"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./includes</directory>
</include>
<exclude>
<directory>./vendor</directory>
<directory>./tests</directory>
</exclude>
<report>
<text outputFile="php://stdout" showUncoveredFiles="false"/>
</report>
</coverage>

<testsuites>
<testsuite name="Unit Tests">
<directory>./tests/Unit</directory>
</testsuite>
<testsuite name="Integration Tests">
<directory>./tests/Integration</directory>
</testsuite>
</testsuites>

<php>
<ini name="display_errors" value="1"/>
<ini name="error_reporting" value="-1"/>
<ini name="memory_limit" value="256M"/>
<const name="COMMUNITY_LISTINGS_TESTING" value="true"/>
</php>
</phpunit>
61 changes: 61 additions & 0 deletions community-listings/tests/Integration/CptRegistrarGraphQLTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* Integration tests for CptRegistrar's WPGraphQL meta type registration.
*
* WPGraphQL's TypeRegistry does not tolerate register_graphql_object_type()/
* register_graphql_field() being invoked more than once per process for the
* same type (a real DUPLICATE_FIELD-class error, the same class of bug this
* plugin already guards against elsewhere for graphql-shortcode-support) — so
* only ONE test in this class performs the actual registration + schema
* build; every other test asserts hook wiring only, matching the pattern
* already proven in silver-assist-security's own GraphQL integration tests.
*
* @package SilverAssist\CommunityListings\Tests\Integration
*/

namespace SilverAssist\CommunityListings\Tests\Integration;

use SilverAssist\CommunityListings\Service\CptRegistrar;
use SilverAssist\PluginKernel\Testing\TestCase;

/**
* @covers \SilverAssist\CommunityListings\Service\CptRegistrar
*/
class CptRegistrarGraphQLTest extends TestCase {

private CptRegistrar $registrar;

protected function setUp(): void {
parent::setUp();

if ( ! \class_exists( 'WPGraphQL' ) ) {
$this->markTestSkipped( 'WPGraphQL plugin not available in the test environment.' );
}

$this->registrar = CptRegistrar::instance();
CptRegistrar::register_post_type();
}

public function test_init_registers_graphql_register_types_hook(): void {
$this->registrar->init();

$this->assertNotFalse( \has_action( 'graphql_register_types', [ $this->registrar, 'register_graphql_meta_fields' ] ) );
}

/**
* The only test in this class that actually performs live WPGraphQL type
* registration — see class docblock for why it must stay singular.
*/
public function test_register_graphql_meta_fields_builds_queryable_schema(): void {
$this->registrar->register_graphql_meta_fields();

// A plain data query (not introspection, which WPGraphQL blocks for public
// requests by default) is enough to force a real schema build.
$result = \graphql( [ 'query' => '{ communities { nodes { id } } }' ] );

$this->assertArrayNotHasKey( 'errors', $result, 'Schema must build successfully after register_graphql_meta_fields() runs.' );

$type = \WPGraphQL::get_type_registry()->get_type( 'CommunityMeta' );
$this->assertNotNull( $type, 'CommunityMeta GraphQL object type should be registered.' );
}
}
83 changes: 83 additions & 0 deletions community-listings/tests/Integration/GraphQLResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php
/**
* Integration tests for GraphQLResolver.
*
* @package SilverAssist\CommunityListings\Tests\Integration
*/

namespace SilverAssist\CommunityListings\Tests\Integration;

use SilverAssist\CommunityListings\Service\GraphQLResolver;
use SilverAssist\PluginKernel\Testing\TestCase;

/**
* @covers \SilverAssist\CommunityListings\Service\GraphQLResolver
*/
class GraphQLResolverTest extends TestCase {

private GraphQLResolver $resolver;

protected function setUp(): void {
parent::setUp();

if ( ! \class_exists( 'WPGraphQL' ) ) {
$this->markTestSkipped( 'WPGraphQL plugin not available in the test environment.' );
}

$this->resolver = GraphQLResolver::instance();
}

public function test_init_registers_resolve_field_filter(): void {
$this->resolver->init();

$this->assertNotFalse( \has_filter( 'graphql_resolve_field', [ $this->resolver, 'resolve_shortcodes' ] ) );
}

public function test_init_registers_graphql_register_types_action(): void {
$this->resolver->init();

$this->assertNotFalse( \has_action( 'graphql_register_types', [ $this->resolver, 'register_rendered_content' ] ) );
}

public function test_resolve_shortcodes_only_applies_to_community_content_and_excerpt(): void {
\add_shortcode( 'cl_test_shortcode', fn() => 'RENDERED' );

$raw = 'Text [cl_test_shortcode] here';

$community_content = $this->resolver->resolve_shortcodes( $raw, null, [], null, null, 'Community', 'content', null, null );
$this->assertSame( 'Text RENDERED here', $community_content );

$other_type = $this->resolver->resolve_shortcodes( $raw, null, [], null, null, 'Post', 'content', null, null );
$this->assertSame( $raw, $other_type, 'Non-Community types must not have shortcodes processed.' );

$other_field = $this->resolver->resolve_shortcodes( $raw, null, [], null, null, 'Community', 'title', null, null );
$this->assertSame( $raw, $other_field, 'Fields other than content/excerpt must not have shortcodes processed.' );

\remove_shortcode( 'cl_test_shortcode' );
}

public function test_resolve_shortcodes_skips_content_without_brackets(): void {
$plain = 'No shortcodes in this text.';

$result = $this->resolver->resolve_shortcodes( $plain, null, [], null, null, 'Community', 'content', null, null );

$this->assertSame( $plain, $result );
}

public function test_register_rendered_content_skips_when_graphql_shortcode_support_present(): void {
if ( ! \class_exists( \SilverAssist\GraphQLShortcodeSupport\Core\Plugin::class ) ) {
$this->markTestSkipped( 'graphql-shortcode-support is not loaded in this test run; duplicate-field guard is not exercised here.' );
}

// The method returns early via class_exists() before calling register_graphql_field() —
// this just documents/exercises that early-return path without a fatal. Calling
// register_graphql_field() unconditionally here (the "absent" branch) is deliberately
// NOT exercised as a second test: WPGraphQL's TypeRegistry treats a repeated direct
// call as a real duplicate field registration (DUPLICATE_FIELD), so it can only safely
// run once per process — the hook-registration assertion above already covers that
// the callback is wired correctly for the natural single schema-build call.
$this->resolver->register_rendered_content();

$this->assertTrue( true );
}
}
28 changes: 28 additions & 0 deletions community-listings/tests/Unit/ActivatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
* Tests for Activator.
*
* @package SilverAssist\CommunityListings\Tests\Unit
*/

namespace SilverAssist\CommunityListings\Tests\Unit;

use SilverAssist\CommunityListings\Core\Activator;
use SilverAssist\PluginKernel\Testing\TestCase;

/**
* @covers \SilverAssist\CommunityListings\Core\Activator
*/
class ActivatorTest extends TestCase {

public function test_activate_registers_community_post_type(): void {
Activator::activate();

$this->assertTrue( \post_type_exists( 'community' ) );

$post_type_object = \get_post_type_object( 'community' );
$this->assertNotNull( $post_type_object );
$this->assertTrue( $post_type_object->hierarchical );
$this->assertSame( 'community', $post_type_object->graphql_single_name );
}
}
Loading