You know you are taking unit test seriously when your unit tests contain 5X the amount of code as the code being tested.
Here is today’s unit test. Sometimes I amaze myself.
<?php
declare( strict_types=1 );
namespace Lipe\Project\Cron;
use DateTimeImmutable;
use Lipe\Lib\Cron\One_Time;
use Lipe\Lib\Post_Type\Wp_Insert_Post;
use Lipe\Lib\Util\Arrays;
use Lipe\Project\Email;
use Lipe\Project\Email\EmailAddress;
use Lipe\Project\Meta\My_Content_Fields;
use Lipe\Project\Meta\User_Fields;
use Lipe\Project\Post_Types\My_Content;
use Lipe\Project\Sms;
use Lipe\Project\Sms\PhoneNumber;
use Lipe\Project\Taxonomies\Graduation_Year;
use Lipe\Project\User\Grade\Wp_Users;
use Lipe\Project\User\Roles;
use Lipe\Project\User\Roles\Customer;
use Lipe\Project\User\Student;
use Lipe\Project\User\User;
use Lipe\WP_Unit\Generators\Template_String;
use Lipe\WP_Unit\Helpers\Snapshots\Adjuster;
use Lipe\WP_Unit\Utils\PrivateAccess;
/**
* @author Mat Lipe
* @since November 2025
*
*/
class My_Content_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_maybe_send_notification_when_already_published(): void {
$post = self::factory()->post->create_and_get( [
'post_type' => My_Content::NAME,
'post_status' => Wp_Insert_Post::STATUS_PUBLISH,
] );
$object = My_Content::factory( $post );
$object[ My_Content_Fields::NOTIFICATION ] = 'Test notification';
My_Content_Notification::in()->maybe_send_notification( $post, Wp_Insert_Post::STATUS_PUBLISH );
$this->assertFalse( wp_next_scheduled( My_Content_Notification::NAME, [ $object ] ) );
}
public function test_maybe_send_notification_when_not_publishing(): void {
$post = self::factory()->post->create_and_get( [
'post_type' => My_Content::NAME,
'post_status' => Wp_Insert_Post::STATUS_DRAFT,
] );
$object = My_Content::factory( $post );
$object[ My_Content_Fields::NOTIFICATION ] = 'Test notification';
My_Content_Notification::in()->maybe_send_notification( $post, Wp_Insert_Post::STATUS_DRAFT );
$this->assertFalse( wp_next_scheduled( My_Content_Notification::NAME, [ $object ] ) );
}
public function test_maybe_send_notification_when_user_uploaded(): void {
$post = self::factory()->post->create_and_get( [
'post_type' => My_Content::NAME,
'post_status' => Wp_Insert_Post::STATUS_PUBLISH,
] );
$object = My_Content::factory( $post );
$object[ My_Content_Fields::USER_UPLOADED ] = true;
$object[ My_Content_Fields::NOTIFICATION ] = 'Test notification';
My_Content_Notification::in()->maybe_send_notification( $post, Wp_Insert_Post::STATUS_DRAFT );
$this->assertFalse( wp_next_scheduled( My_Content_Notification::NAME, [ $object ] ) );
}
public function test_maybe_send_notification_when_notification_empty(): void {
$post = self::factory()->post->create_and_get( [
'post_type' => My_Content::NAME,
'post_status' => Wp_Insert_Post::STATUS_PUBLISH,
] );
$object = My_Content::factory( $post );
My_Content_Notification::in()->maybe_send_notification( $post, Wp_Insert_Post::STATUS_DRAFT );
$this->assertFalse( wp_next_scheduled( My_Content_Notification::NAME, [ $object ] ) );
}
public function test_maybe_send_notification_schedules_when_valid(): void {
$post = self::factory()->post->create_and_get( [
'post_type' => My_Content::NAME,
'post_status' => Wp_Insert_Post::STATUS_PUBLISH,
] );
$object = My_Content::factory( $post );
$object[ My_Content_Fields::NOTIFICATION ] = 'Test notification';
My_Content_Notification::in()->maybe_send_notification( $post, Wp_Insert_Post::STATUS_DRAFT );
$time = wp_next_scheduled( My_Content_Notification::NAME, [ $object ] );
$this->assertNotFalse( $time );
$this->assertSame( $time, One_Time::factory( My_Content_Notification::in() )->get_next_run( $object ) );
}
public function test_get_matching_users(): void {
$user_ids = $this->get_students();
$matches_2025 = \iterator_to_array( PrivateAccess::in()->call_private_method( My_Content_Notification::in(), 'get_users_to_notify', [ new DateTimeImmutable( '2025-01-01' ) ] ) );
$matches_2028 = \iterator_to_array( PrivateAccess::in()->call_private_method( My_Content_Notification::in(), 'get_users_to_notify', [ new DateTimeImmutable( '2028-01-01' ) ] ) );
$this->assertCount( 6, $matches_2025 );
$this->assertCount( 3, $matches_2028 );
$this->assertNotSame( $matches_2025, $matches_2028 );
$twenty_five = [
User::factory( $user_ids['2025 - 1'] )->get_parent()->get_id(),
User::factory( $user_ids['2025 - 2'] )->get_parent()->get_id(),
User::factory( $user_ids['2025 - 3'] )->get_parent()->get_id(),
User::factory( $user_ids['2025 - 4'] )->get_parent()->get_id(),
$user_ids['2025 - 2'],
$user_ids['2025 - 3'],
];
foreach ( $matches_2025 as $user ) {
$this->assertInstanceOf( User::class, $user );
$this->assertContains( $user->get_id(), $twenty_five );
}
$this->assertCount( 6, \array_unique( $twenty_five ) );
$twenty_eight = [
User::factory( $user_ids['2028 - 0'] )->get_parent()->get_id(),
User::factory( $user_ids['2028 - 1'] )->get_parent()->get_id(),
$user_ids['2028 - 1'],
$user_ids['2028 - 2'],
];
foreach ( $matches_2028 as $user ) {
$this->assertInstanceOf( User::class, $user );
$this->assertContains( $user->get_id(), $twenty_eight );
}
$this->assertCount( 3, \array_unique( $twenty_eight ) );
}
public function test_queued_emails(): void {
$content = \file_get_contents( dirname( __DIR__, 2 ) . '/fixtures/emails/my-content-notification.html' );
$user_ids = $this->trigger_publish_notifications();
$queue = Email\Queue\Db::in()->get_emails( Email\Queue\Status::PENDING );
$this->assertEmpty( $queue );
wp_cron_run_event( My_Content_Notification::NAME );
$this->assertEmpty( Email\Queue\Db::in()->get_emails( Email\Queue\Status::SKIPPED ) );
$queue = Email\Queue\Db::in()->get_emails( Email\Queue\Status::PENDING );
$adjuster = Adjuster::create()
->replace( 'recipient', fn( EmailAddress $address ) => $address->get_email() )
->replace( 'content', fn( string $content ) => \preg_replace( '/selected=\d+/', 'selected=25', $content ) )
->replace( 'type', fn( Email\Queue\Type $type ) => $type->value );
$this->assertSameIgnoreLeadingWhitespace( [
// Parent of text only users will still be notified their email notification is on.
[
'recipient' => User::factory( $user_ids['2025 - 3'] )->user_email,
'subject' => 'My Tested Content - Now Available',
'type' => Email\Queue\Type::NEW_MY_CONTENT->value,
'content' => $content,
],
[
'recipient' => User::factory( $user_ids['2025 - 1'] )->get_parent()->user_email,
'subject' => 'My Tested Content - Now Available',
'type' => Email\Queue\Type::NEW_MY_CONTENT->value,
'content' => $content,
],
[
'recipient' => User::factory( $user_ids['2025 - 2'] )->get_parent()->user_email,
'subject' => 'My Tested Content - Now Available',
'type' => Email\Queue\Type::NEW_MY_CONTENT->value,
'content' => $content,
],
[
'recipient' => User::factory( $user_ids['2025 - 4'] )->get_parent()->user_email,
'subject' => 'My Tested Content - Now Available',
'type' => Email\Queue\Type::NEW_MY_CONTENT->value,
'content' => $content,
],
[
'recipient' => User::factory( $user_ids['2028 - 1'] )->user_email,
'subject' => 'My Tested Content - Now Available',
'type' => Email\Queue\Type::NEW_MY_CONTENT->value,
'content' => $content,
],
[
'recipient' => User::factory( $user_ids['2028 - 0'] )->get_parent()->user_email,
'subject' => 'My Tested Content - Now Available',
'type' => Email\Queue\Type::NEW_MY_CONTENT->value,
'content' => $content,
],
], \array_map( $adjuster, Arrays::in()->list_pluck( $queue, [ 'recipient', 'subject', 'type', 'content' ]
) ) );
}
public function test_queued_emails_with_batch_size(): void {
PrivateAccess::in()->set_private_property( My_Content_Notification::in(), 'batch_size', 2 );
$this->test_queued_emails();
}
public function test_queued_sms(): void {
$user_ids = $this->trigger_publish_notifications();
$this->assertEmpty( Sms\Queue\Db::in()->get_sms( Sms\Queue\Status::PENDING ) );
wp_cron_run_event( My_Content_Notification::NAME );
$this->assertEmpty( Sms\Queue\Db::in()->get_sms( Sms\Queue\Status::SKIPPED ) );
$queue = Sms\Queue\Db::in()->get_sms( Sms\Queue\Status::PENDING );
$content = 'My Tested Content fa la la. Outside https://aspiring-higher-dev.matlipe.com/my-content?selected=25';
$adjuster = Adjuster::create()
->replace( 'phone', fn( PhoneNumber $number ) => $number->get_number() )
->replace( 'message', fn( string $message ) => \preg_replace( '/selected=\d+/', 'selected=25', $message ) )
->replace( 'type', fn( Sms\Queue\Type $type ) => $type->value );
$this->assertSame( [
// Parent of text and email user will still be notified their text notifications are on.
[
'phone' => User::factory( $user_ids['2025 - 2'] )[ User_Fields::PHONE ],
'message' => $content,
'user_id' => $user_ids['2025 - 2'],
'type' => Sms\Queue\Type::NEW_MY_CONTENT->value,
],
[
'phone' => User::factory( $user_ids['2025 - 2'] )->get_parent()[ User_Fields::PHONE ],
'message' => $content,
'user_id' => User::factory( $user_ids['2025 - 2'] )->get_parent()->get_id(),
'type' => Sms\Queue\Type::NEW_MY_CONTENT->value,
],
[
'phone' => User::factory( $user_ids['2025 - 3'] )->get_parent()[ User_Fields::PHONE ],
'message' => $content,
'user_id' => User::factory( $user_ids['2025 - 3'] )->get_parent()->get_id(),
'type' => Sms\Queue\Type::NEW_MY_CONTENT->value,
],
[
'phone' => User::factory( $user_ids['2025 - 4'] )->get_parent()[ User_Fields::PHONE ],
'message' => $content,
'user_id' => User::factory( $user_ids['2025 - 4'] )->get_parent()->get_id(),
'type' => Sms\Queue\Type::NEW_MY_CONTENT->value,
],
[
'phone' => User::factory( $user_ids['2028 - 1'] )[ User_Fields::PHONE ],
'message' => $content,
'user_id' => $user_ids['2028 - 1'],
'type' => Sms\Queue\Type::NEW_MY_CONTENT->value,
],
[
'phone' => User::factory( $user_ids['2028 - 2'] )[ User_Fields::PHONE ],
'message' => $content,
'user_id' => $user_ids['2028 - 2'],
'type' => Sms\Queue\Type::NEW_MY_CONTENT->value,
],
[
'phone' => User::factory( $user_ids['2028 - 1'] )->get_parent()[ User_Fields::PHONE ],
'message' => $content,
'user_id' => User::factory( $user_ids['2028 - 1'] )->get_parent()->get_id(),
'type' => Sms\Queue\Type::NEW_MY_CONTENT->value,
],
], \array_map( $adjuster, Arrays::in()->list_pluck( $queue, [ 'phone', 'message', 'user_id', 'type' ] ) ) );
}
public function test_get_matching_users_incremented(): void {
$mock = $this->getMockBuilder( \wpdb::class )
->setConstructorArgs( [ DB_USER, DB_PASSWORD, DB_NAME, DB_HOST ] )
->onlyMethods( [ 'get_col' ] )
->getMock();
$mock->expects( $this->exactly( 3 ) )
->method( 'get_col' )
->willReturnCallback( function( $query ) {
static $call_count = 0;
$call_count ++;
if ( 1 === $call_count ) {
$this->assertSameIgnoreEOL(
"SELECT DISTINCT wp_users.ID
FROM wp_users LEFT JOIN wp_usermeta ON ( wp_users.ID = wp_usermeta.user_id AND wp_usermeta.meta_key = 'lipe/project/meta/user-fields/text-notifications' ) LEFT JOIN wp_usermeta AS mt1 ON ( wp_users.ID = mt1.user_id AND mt1.meta_key = 'lipe/project/meta/user-fields/email-notifications' )
WHERE 1=1 AND (
wp_usermeta.user_id IS NULL
OR
mt1.user_id IS NULL
) AND wp_users.graduation_year = '2028-01-01'
ORDER BY display_name ASC
LIMIT 0, 500", $query );
return \array_fill( 0, 500, 1 );
}
if ( 2 === $call_count ) {
$this->assertSameIgnoreEOL( "SELECT DISTINCT wp_users.ID
FROM wp_users LEFT JOIN wp_usermeta ON ( wp_users.ID = wp_usermeta.user_id AND wp_usermeta.meta_key = 'lipe/project/meta/user-fields/text-notifications' ) LEFT JOIN wp_usermeta AS mt1 ON ( wp_users.ID = mt1.user_id AND mt1.meta_key = 'lipe/project/meta/user-fields/email-notifications' )
WHERE 1=1 AND (
wp_usermeta.user_id IS NULL
OR
mt1.user_id IS NULL
) AND wp_users.graduation_year = '2028-01-01'
ORDER BY display_name ASC
LIMIT 500, 500", $query );
return \array_fill( 0, 500, 1 );
}
if ( 3 === $call_count ) {
$this->assertSameIgnoreEOL( "SELECT DISTINCT wp_users.ID
FROM wp_users LEFT JOIN wp_usermeta ON ( wp_users.ID = wp_usermeta.user_id AND wp_usermeta.meta_key = 'lipe/project/meta/user-fields/text-notifications' ) LEFT JOIN wp_usermeta AS mt1 ON ( wp_users.ID = mt1.user_id AND mt1.meta_key = 'lipe/project/meta/user-fields/email-notifications' )
WHERE 1=1 AND (
wp_usermeta.user_id IS NULL
OR
mt1.user_id IS NULL
) AND wp_users.graduation_year = '2028-01-01'
ORDER BY display_name ASC
LIMIT 1000, 500", $query );
return \array_fill( 0, 26, 1 );
}
$this->fail( 'Unexpected call to get_col' );
} );
$mock->set_prefix( 'wp_' );
$GLOBALS['wpdb'] = $mock;
\iterator_to_array( PrivateAccess::in()->call_private_method( My_Content_Notification::in(), 'get_matching_users', [ new DateTimeImmutable( '2028-01-01' ) ] ) );
}
/**
* Trigger the publication notifications by creating a My Content item with
* the graduation year set to 2025 and 2028.
*
* Return the list of students which received or did not receive the
* notification based on their notification settings.
*
* @see self::get_students()
*
* @return array - Result of $this->get_students();
*/
private function trigger_publish_notifications(): array {
wp_set_current_user( 1 );
$user_ids = $this->get_students();
$twenty_3_student = self::factory()->user->create( [
'role' => Roles\Student::NAME,
Wp_Users::GRADUATION_YEAR => '2023-01-01',
] );
User::factory( $twenty_3_student )[ User_Fields::EMAIL_NOTIFICATIONS ] = User_Fields::ONOFF_ON;
self::factory()->term->create( [
'taxonomy' => Graduation_Year::NAME,
'name' => '2023',
] );
$twenty_5 = self::factory()->term->create( [
'taxonomy' => Graduation_Year::NAME,
'name' => '2025',
] );
$twenty_8 = self::factory()->term->create( [
'taxonomy' => Graduation_Year::NAME,
'name' => '2028',
] );
self::factory()->post->create( [
'post_type' => My_Content::NAME,
'post_title' => 'My Tested Content',
'post_status' => Wp_Insert_Post::STATUS_PUBLISH,
'meta_input' => [
My_Content_Fields::NOTIFICATION => '<p>fa la la.</p><h2>Outside</h2>',
],
'tax_input' => [
Graduation_Year::NAME => [ $twenty_5, $twenty_8 ],
],
] );
return $user_ids;
}
/**
* @return array{
* "2023 - 0": int,
* "2025 - 1": int, - parent's text disabled
* "2025 - 2": int, - text enabled
* "2025 - 3": int, - email enabled, parent's email disabled
* "2025 - 4": int,
* "2025 - 5": int, - parent's text and email disabled
* "2028 - 0": int,
* "2028 - 1": int, - text and email enabled
* "2028 - 2": int, - text enabled
* }
*/
private function get_students(): array {
$students = [];
$first_name = new class() implements Template_String {
private int $counter;
public function __construct() {
$this->counter = 0;
}
public function get_template_string(): string {
return 'Parent ' . $this->counter ++;
}
};
$parents = self::factory()->user->create_many( 7, [
'role' => Customer::NAME,
], [
'first_name' => $first_name,
] );
for ( $i = 0; $i < 1; $i ++ ) {
$students["2023 - {$i}"] = self::factory()->user->create( [
'first_name' => "2023 - {$i}",
'role' => Roles\Student::NAME,
Wp_Users::GRADUATION_YEAR => '2023-01-01',
] );
if ( ! Student\Db::in()->add_student( User::factory( $parents[ $i ] ), User::factory( $students["2023 - {$i}"] ) ) ) {
$this->fail( 'Failed to add a student to a parent.' );
}
}
for ( $i = 1; $i < 6; $i ++ ) {
$students["2025 - {$i}"] = self::factory()->user->create( [
'first_name' => "2025 - {$i}",
'role' => Roles\Student::NAME,
Wp_Users::GRADUATION_YEAR => '2025-01-01',
] );
if ( ! Student\Db::in()->add_student( User::factory( $parents[ $i ] ), User::factory( $students["2025 - {$i}"] ) ) ) {
$this->fail( 'Failed to add a student to a parent.' );
}
}
for ( $i = 0; $i < 3; $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[6] ), User::factory( $students["2028 - {$i}"] ) ) ) {
$this->fail( 'Failed to add a student to a parent.' );
}
}
$text_disabled = [
$parents[1],
$parents[5],
$students['2023 - 0'],
$students['2025 - 1'],
$students['2025 - 3'],
$students['2025 - 4'],
$students['2025 - 5'],
$students['2028 - 0'],
];
$email_disabled = [
$parents[3],
$parents[5],
$students['2023 - 0'],
$students['2025 - 1'],
$students['2025 - 2'],
$students['2025 - 4'],
$students['2025 - 5'],
$students['2028 - 0'],
$students['2028 - 2'],
];
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 $students;
}
}
