Mat Lipe Dot Com

Mat Lipe Dot Com

Reflections from a Distinguished Software Engineer

Unit Test – FTW

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;
	}
}
  • About Me
  • Resume
  • My Story
  • Recipes
  • Contact
  • Plugins

Copyright © 2026 ยท Log in