Another unit test for a similar feature.
This test is for a single incomplete PHP class. Clearly, I’m not messing around with this notification system.
<?php
declare( strict_types=1 );
namespace Lipe\Project\Cron;
use Lipe\Project\Meta\User_Fields;
use Lipe\Project\Taxonomies\Graduation_Year;
use Lipe\Project\User\Grade\Wp_Users;
use Lipe\Project\User\Membership;
use Lipe\Project\User\Membership\Plan;
use Lipe\Project\User\Roles;
use Lipe\Project\User\Student;
use Lipe\Project\User\User;
use Lipe\WP_Unit\Utils\PrivateAccess;
use PHPUnit\Framework\Attributes\DataProvider;
/**
* @author Mat Lipe
* @since November 2027
*
*/
class Message_NotificationTest extends \WP_UnitTestCase {
public static \wpdb $wpdb;
protected function setUp(): void {
parent::setUp();
self::$wpdb = $GLOBALS['wpdb'];
}
protected function tearDown(): void {
$GLOBALS['wpdb'] = self::$wpdb;
parent::tearDown();
}
public function test_get_matching_users_incremented(): void {
// prime caches to avoid conflicting queries.
Plan::TEAMS_YEARLY->variation_id();
Plan::TEAMS_MONTHLY->variation_id();
Student\Join_Table::in();
$params = [
[
Graduation_Year::from_year( new \DateTimeImmutable( '2026-01-01' ) ),
Graduation_Year::from_year( new \DateTimeImmutable( '2027-01-01' ) ),
],
[
Plan::TEAMS_YEARLY,
Plan::TEAMS_MONTHLY,
],
];
$mock = $this->getMockBuilder( \wpdb::class )
->setConstructorArgs( [ DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ] )
->onlyMethods( [ 'get_results' ] )
->getMock();
$mock->expects( $this->exactly( 3 ) )
->method( 'get_results' )
->willReturnCallback( function( $query ) {
static $call_count = 0;
$call_count ++;
if ( 1 === $call_count ) {
$this->assertSameIgnoreLeadingWhitespace( "SELECT DISTINCT users.ID as parent_id, students.ID as student_id
FROM `wp_users` AS users
INNER JOIN `wp_wc_orders` AS orders
ON orders.customer_id = users.ID
AND orders.type = 'shop_subscription'
# Limit to active subscriptions.
AND orders.status IN ('wc-active', 'wc-pending-cancel')
INNER JOIN `wp_woocommerce_order_items` as order_items
ON order_items.order_id = orders.id
AND order_items.order_item_type = 'line_item'
INNER JOIN `wp_woocommerce_order_itemmeta` AS itemmeta
ON order_items.order_item_id = itemmeta.order_item_id
AND itemmeta.meta_key IN ('_variation_id', '_product_id')
# Limit to subscriptions to the provided plans.
AND itemmeta.meta_value IN ('" . Plan::TEAMS_YEARLY->variation_id() . "','" . Plan::TEAMS_MONTHLY->variation_id() . "')
# Joins to filter by graduation year.
INNER JOIN `wp_parent_student_relationships` student_relationship
ON student_relationship.parent_id = `users`.ID
INNER JOIN `wp_users` students
ON student_relationship.student_id = students.ID
# Limit to graduation years.
AND students.graduation_year IN ('2026-01-01','2027-01-01')
# Parents join to filter by notifications enabled.
LEFT JOIN `wp_usermeta` as mt0
ON users.ID = mt0.user_id
AND mt0.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `wp_usermeta` AS mt1
ON users.ID = mt1.user_id
AND mt1.meta_key = 'lipe/project/meta/user-fields/email-notifications'
# Students join to filter by notifications enabled.
LEFT JOIN `wp_usermeta` AS mt2
ON students.ID = mt2.user_id
AND mt2.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `wp_usermeta` AS mt3
ON students.ID = mt3.user_id
AND mt3.meta_key = 'lipe/project/meta/user-fields/email-notifications'
WHERE
# Limit to users with notifications enabled.
# May include students or parents who don't have notifications enabled
# if their counterpart has notifications enabled.
(
mt0.user_id IS NULL OR
mt1.user_id IS NULL OR
mt2.user_id IS NULL OR
mt3.user_id IS NULL
)
ORDER BY parent_id LIMIT 500 OFFSET 0", $query );
return \array_fill( 0, 500, 1 );
}
if ( 2 === $call_count ) {
$this->assertSameIgnoreLeadingWhitespace( "SELECT DISTINCT users.ID as parent_id, students.ID as student_id
FROM `wp_users` AS users
INNER JOIN `wp_wc_orders` AS orders
ON orders.customer_id = users.ID
AND orders.type = 'shop_subscription'
# Limit to active subscriptions.
AND orders.status IN ('wc-active', 'wc-pending-cancel')
INNER JOIN `wp_woocommerce_order_items` as order_items
ON order_items.order_id = orders.id
AND order_items.order_item_type = 'line_item'
INNER JOIN `wp_woocommerce_order_itemmeta` AS itemmeta
ON order_items.order_item_id = itemmeta.order_item_id
AND itemmeta.meta_key IN ('_variation_id', '_product_id')
# Limit to subscriptions to the provided plans.
AND itemmeta.meta_value IN ('" . Plan::TEAMS_YEARLY->variation_id() . "','" . Plan::TEAMS_MONTHLY->variation_id() . "')
# Joins to filter by graduation year.
INNER JOIN `wp_parent_student_relationships` student_relationship
ON student_relationship.parent_id = `users`.ID
INNER JOIN `wp_users` students
ON student_relationship.student_id = students.ID
# Limit to graduation years.
AND students.graduation_year IN ('2026-01-01','2027-01-01')
# Parents join to filter by notifications enabled.
LEFT JOIN `wp_usermeta` as mt0
ON users.ID = mt0.user_id
AND mt0.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `wp_usermeta` AS mt1
ON users.ID = mt1.user_id
AND mt1.meta_key = 'lipe/project/meta/user-fields/email-notifications'
# Students join to filter by notifications enabled.
LEFT JOIN `wp_usermeta` AS mt2
ON students.ID = mt2.user_id
AND mt2.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `wp_usermeta` AS mt3
ON students.ID = mt3.user_id
AND mt3.meta_key = 'lipe/project/meta/user-fields/email-notifications'
WHERE
# Limit to users with notifications enabled.
# May include students or parents who don't have notifications enabled
# if their counterpart has notifications enabled.
(
mt0.user_id IS NULL OR
mt1.user_id IS NULL OR
mt2.user_id IS NULL OR
mt3.user_id IS NULL
)
ORDER BY parent_id LIMIT 500 OFFSET 500", $query );
return \array_fill( 0, 500, 1 );
}
if ( 3 === $call_count ) {
$this->assertSameIgnoreLeadingWhitespace( "SELECT DISTINCT users.ID as parent_id, students.ID as student_id
FROM `wp_users` AS users
INNER JOIN `wp_wc_orders` AS orders
ON orders.customer_id = users.ID
AND orders.type = 'shop_subscription'
# Limit to active subscriptions.
AND orders.status IN ('wc-active', 'wc-pending-cancel')
INNER JOIN `wp_woocommerce_order_items` as order_items
ON order_items.order_id = orders.id
AND order_items.order_item_type = 'line_item'
INNER JOIN `wp_woocommerce_order_itemmeta` AS itemmeta
ON order_items.order_item_id = itemmeta.order_item_id
AND itemmeta.meta_key IN ('_variation_id', '_product_id')
# Limit to subscriptions to the provided plans.
AND itemmeta.meta_value IN ('" . Plan::TEAMS_YEARLY->variation_id() . "','" . Plan::TEAMS_MONTHLY->variation_id() . "')
# Joins to filter by graduation year.
INNER JOIN `wp_parent_student_relationships` student_relationship
ON student_relationship.parent_id = `users`.ID
INNER JOIN `wp_users` students
ON student_relationship.student_id = students.ID
# Limit to graduation years.
AND students.graduation_year IN ('2026-01-01','2027-01-01')
# Parents join to filter by notifications enabled.
LEFT JOIN `wp_usermeta` as mt0
ON users.ID = mt0.user_id
AND mt0.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `wp_usermeta` AS mt1
ON users.ID = mt1.user_id
AND mt1.meta_key = 'lipe/project/meta/user-fields/email-notifications'
# Students join to filter by notifications enabled.
LEFT JOIN `wp_usermeta` AS mt2
ON students.ID = mt2.user_id
AND mt2.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `wp_usermeta` AS mt3
ON students.ID = mt3.user_id
AND mt3.meta_key = 'lipe/project/meta/user-fields/email-notifications'
WHERE
# Limit to users with notifications enabled.
# May include students or parents who don't have notifications enabled
# if their counterpart has notifications enabled.
(
mt0.user_id IS NULL OR
mt1.user_id IS NULL OR
mt2.user_id IS NULL OR
mt3.user_id IS NULL
)
ORDER BY parent_id LIMIT 500 OFFSET 1000", $query );
return \array_fill( 0, 26, 1 );
}
$this->fail( 'Unexpected call to get_results' );
} );
$mock->set_prefix( 'wp_' );
$GLOBALS['wpdb'] = $mock;
\iterator_to_array( PrivateAccess::in()->call_private_method( Message_Notification::in(), 'get_users_to_notify', $params ) );
}
#[DataProvider( 'provideUsersToNotify' )]
public function test_get_users_to_notify( Plan $plan, \DateTimeImmutable $year, array $user_keys ): void {
$users = $this->populate_users();
/** @var User[] $results */
$results = \iterator_to_array( PrivateAccess::in()->call_private_method( Message_Notification::in(), 'get_users_to_notify', [ [ Graduation_Year::from_year( $year ) ], [ $plan ] ] ) );
foreach ( $results as $user ) {
wp_set_current_user( $user->get_id() );
Membership::in()->clear_memoize_cache();
$this->assertSame( $plan, Plan::get_current_user_plan() );
$years = $user->get_graduation_years();
$this->assertSame( $year->format( 'Y' ), (string) \reset( $years )->get_year() );
}
$user_ids = \array_map( fn( User $user ) => $user->get_id(), $results );
$from_keys = \array_map( fn( string $key ) => $users[ $key ], $user_keys );
$this->assertEquals( $from_keys, \array_values( $user_ids ) );
}
#[DataProvider( 'provideQueries' )]
public function test_get_query( array $plans, \DateTimeImmutable $year, string $expected, int $page ): void {
$grad_year = Graduation_Year::from_year( $year );
$query = PrivateAccess::in()->call_private_method( Message_Notification::in(), 'get_query', [ [ $grad_year ], $plans, $page ] );
$replaced = \str_replace( '{plan_id}', \implode( "','", \array_map( fn( Plan $plan ) => (string) $plan->variation_id(), $plans ) ), $expected );
$this->assertSameIgnoreLeadingWhitespace( $replaced, $query );
}
/**
* @return array{
* "2019 - 0": int, - both enabled
* "2027 - 1": int, - both disabled
* "2027 - 2": int, - text enabled
* "2027 - 3": int, - email enabled
* "2027 - 4": int, - both disabled
* "2027 - 5": int, - both disabled
* "2028 - 6": int, - both disabled
* "2028 - 7": int, - both enabled
* "2028 - 8": int, - text enabled
* "parent-0": int, - both enabled (Plan::FREE_YEARLY)
* "parent-1": int, - text disabled (Plan::FREE_YEARLY)
* "parent-2": int, - both enabled (No plan)
* "parent-3": int, - email disabled (Plan::TEAMS_YEARLY)
* "parent-4": int, - both enabled (Plan::TEAMS_YEARLY)
* "parent-5": int, - both disabled (Plan::TEAMS_YEARLY)
* "parent-6": int, - both disabled (Plan::PLUS_MONTHLY)
* "parent-7": int, - both enabled (Plan::TEAMS_YEARLY)
* "parent-2028": int, - both enabled (Plan::TEAMS_YEARLY) (parent for all 2028 students)
* }
*/
private function populate_users(): array {
$students = [];
$parents = [];
for ( $i = 0; $i < 8; $i ++ ) {
$parents["parent-{$i}"] = self::factory()->user->create( [
'first_name' => "Parent - {$i}",
'role' => Roles\Subscriber::NAME,
] );
wp_set_current_user( $parents["parent-{$i}"] );
switch ( $i ) {
case 0:
case 1:
tests_generate_order_with_plan( Plan::FREE_YEARLY );
break;
case 3:
case 4:
case 5:
case 7:
tests_generate_order_with_plan( Plan::TEAMS_YEARLY );
break;
case 6:
tests_generate_order_with_plan( Plan::PLUS_MONTHLY );
}
}
$parents['parent-2028'] = $parents["parent-7"];
for ( $i = 0; $i < 1; $i ++ ) {
$students["2019 - {$i}"] = self::factory()->user->create( [
'first_name' => "2019 - {$i}",
'role' => Roles\Student::NAME,
Wp_Users::GRADUATION_YEAR => '2019-01-01',
] );
if ( ! Student\Db::in()->add_student( User::factory( $parents['parent-0'] ), User::factory( $students["2019 - {$i}"] ) ) ) {
$this->fail( 'Failed to add a student to a parent.' );
}
}
for ( $i = 1; $i < 6; $i ++ ) {
$students["2027 - {$i}"] = self::factory()->user->create( [
'first_name' => "2027 - {$i}",
'role' => Roles\Student::NAME,
Wp_Users::GRADUATION_YEAR => '2027-01-01',
] );
if ( ! Student\Db::in()->add_student( User::factory( $parents["parent-{$i}"] ), User::factory( $students["2027 - {$i}"] ) ) ) {
$this->fail( 'Failed to add a student to a parent.' );
}
}
for ( $i = 6; $i < 10; $i ++ ) {
$students["2028 - {$i}"] = self::factory()->user->create( [
'first_name' => "2028 - {$i}",
'role' => Roles\Student::NAME,
Wp_Users::GRADUATION_YEAR => '2028-01-01',
] );
if ( ! Student\Db::in()->add_student( User::factory( $parents['parent-2028'] ), User::factory( $students["2028 - {$i}"] ) ) ) {
$this->fail( 'Failed to add a student to a parent.' );
}
}
$text_disabled = [
$parents['parent-1'],
$parents['parent-5'],
$students['2019 - 0'],
$students['2027 - 1'],
$students['2027 - 3'],
$students['2027 - 4'],
$students['2027 - 5'],
$students['2028 - 6'],
];
$email_disabled = [
$parents['parent-3'],
$parents['parent-5'],
$students['2019 - 0'],
$students['2027 - 1'],
$students['2027 - 2'],
$students['2027 - 4'],
$students['2027 - 5'],
$students['2028 - 6'],
$students['2028 - 8'],
];
foreach ( $text_disabled as $student ) {
User::factory( $student )[ User_Fields::TEXT_NOTIFICATIONS ] = User_Fields::ONOFF_OFF;
}
foreach ( $email_disabled as $student ) {
User::factory( $student )[ User_Fields::EMAIL_NOTIFICATIONS ] = User_Fields::ONOFF_OFF;
}
foreach ( \array_values( [ ...$students, ...$parents ] ) as $i => $student ) {
$v = \str_pad( (string) $i, 2, '0', STR_PAD_LEFT );
User::factory( $student )[ User_Fields::PHONE ] = \sprintf( '155555555%s', $v );
}
return [ ...$parents, ...$students ];
}
public static function provideUsersToNotify(): array {
return [
'2027 - FREE_YEARLY' => [
'plan' => Plan::FREE_YEARLY,
'year' => new \DateTimeImmutable( '2027-01-01' ),
'user_keys' => [
'parent-1',
'2027 - 1',
],
],
'2027 - TEAMS_YEARLY' => [
'plan' => Plan::TEAMS_YEARLY,
'year' => new \DateTimeImmutable( '2027-01-01' ),
'user_keys' => [
'parent-3',
'2027 - 3',
'parent-4',
'2027 - 4',
],
],
];
}
public static function provideQueries(): array {
return [
'2027 - TEAMS_YEARLY' => [
'plans' => [ Plan::TEAMS_YEARLY, Plan::TEAMS_MONTHLY ],
'year' => new \DateTimeImmutable( '2027-01-01' ),
'page' => 1,
'expected' => "SELECT DISTINCT users.ID as parent_id, students.ID as student_id
FROM `z_tests_users` AS users
INNER JOIN `z_tests_wc_orders` AS orders
ON orders.customer_id = users.ID
AND orders.type = 'shop_subscription'
# Limit to active subscriptions.
AND orders.status IN ('wc-active', 'wc-pending-cancel')
INNER JOIN `z_tests_woocommerce_order_items` as order_items
ON order_items.order_id = orders.id
AND order_items.order_item_type = 'line_item'
INNER JOIN `z_tests_woocommerce_order_itemmeta` AS itemmeta
ON order_items.order_item_id = itemmeta.order_item_id
AND itemmeta.meta_key IN ('_variation_id', '_product_id')
# Limit to subscriptions to the provided plans.
AND itemmeta.meta_value IN ('{plan_id}')
# Joins to filter by graduation year.
INNER JOIN `z_tests_parent_student_relationships` student_relationship
ON student_relationship.parent_id = `users`.ID
INNER JOIN `z_tests_users` students
ON student_relationship.student_id = students.ID
# Limit to graduation years.
AND students.graduation_year IN ('2027-01-01')
# Parents join to filter by notifications enabled.
LEFT JOIN `z_tests_usermeta` as mt0
ON users.ID = mt0.user_id
AND mt0.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `z_tests_usermeta` AS mt1
ON users.ID = mt1.user_id
AND mt1.meta_key = 'lipe/project/meta/user-fields/email-notifications'
# Students join to filter by notifications enabled.
LEFT JOIN `z_tests_usermeta` AS mt2
ON students.ID = mt2.user_id
AND mt2.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `z_tests_usermeta` AS mt3
ON students.ID = mt3.user_id
AND mt3.meta_key = 'lipe/project/meta/user-fields/email-notifications'
WHERE
# Limit to users with notifications enabled.
# May include students or parents who don't have notifications enabled
# if their counterpart has notifications enabled.
(
mt0.user_id IS NULL OR
mt1.user_id IS NULL OR
mt2.user_id IS NULL OR
mt3.user_id IS NULL
)
ORDER BY parent_id LIMIT 500 OFFSET 0",
],
'2029 - Plus Monthly' => [
'plans' => [ Plan::PLUS_MONTHLY ],
'year' => new \DateTimeImmutable( '2029-01-01' ),
'page' => 3,
'expected' => "SELECT DISTINCT users.ID as parent_id, students.ID as student_id
FROM `z_tests_users` AS users
INNER JOIN `z_tests_wc_orders` AS orders
ON orders.customer_id = users.ID
AND orders.type = 'shop_subscription'
# Limit to active subscriptions.
AND orders.status IN ('wc-active', 'wc-pending-cancel')
INNER JOIN `z_tests_woocommerce_order_items` as order_items
ON order_items.order_id = orders.id
AND order_items.order_item_type = 'line_item'
INNER JOIN `z_tests_woocommerce_order_itemmeta` AS itemmeta
ON order_items.order_item_id = itemmeta.order_item_id
AND itemmeta.meta_key IN ('_variation_id', '_product_id')
# Limit to subscriptions to the provided plans.
AND itemmeta.meta_value IN ('{plan_id}')
# Joins to filter by graduation year.
INNER JOIN `z_tests_parent_student_relationships` student_relationship
ON student_relationship.parent_id = `users`.ID
INNER JOIN `z_tests_users` students
ON student_relationship.student_id = students.ID
# Limit to graduation years.
AND students.graduation_year IN ('2029-01-01')
# Parents join to filter by notifications enabled.
LEFT JOIN `z_tests_usermeta` as mt0
ON users.ID = mt0.user_id
AND mt0.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `z_tests_usermeta` AS mt1
ON users.ID = mt1.user_id
AND mt1.meta_key = 'lipe/project/meta/user-fields/email-notifications'
# Students join to filter by notifications enabled.
LEFT JOIN `z_tests_usermeta` AS mt2
ON students.ID = mt2.user_id
AND mt2.meta_key = 'lipe/project/meta/user-fields/text-notifications'
LEFT JOIN `z_tests_usermeta` AS mt3
ON students.ID = mt3.user_id
AND mt3.meta_key = 'lipe/project/meta/user-fields/email-notifications'
WHERE
# Limit to users with notifications enabled.
# May include students or parents who don't have notifications enabled
# if their counterpart has notifications enabled.
(
mt0.user_id IS NULL OR
mt1.user_id IS NULL OR
mt2.user_id IS NULL OR
mt3.user_id IS NULL
)
ORDER BY parent_id LIMIT 500 OFFSET 1000",
],
];
}
}
