@@ -551,11 +551,143 @@ public function test_notify_expired($notification_type, $post_data, $expected_us
551551 }
552552 }
553553
554+ /**
555+ * @dataProvider data_notification_webpush
556+ */
557+ public function test_expired_subscriptions_deleted ($ notification_type , $ post_data , $ expected_users )
558+ {
559+ // Skip test if no expected users
560+ if (empty ($ expected_users ))
561+ {
562+ $ this ->assertTrue (true );
563+ return ;
564+ }
565+
566+ $ subscription_info = [];
567+ foreach ($ expected_users as $ user_id => $ user_data )
568+ {
569+ $ subscription_info [$ user_id ][] = $ this ->create_subscription_for_user ($ user_id );
570+ }
571+
572+ // Get first user and expire their subscription
573+ $ first_user_id = array_key_first ($ expected_users );
574+ $ first_user_sub = $ subscription_info [$ first_user_id ][0 ];
575+ $ this ->expire_subscription ($ first_user_sub ['clientHash ' ]);
576+
577+ // Count subscriptions before notification
578+ $ subscriptions_before = $ this ->get_subscription_count ();
579+ $ this ->assertEquals (count ($ expected_users ), $ subscriptions_before , 'Expected ' . count ($ expected_users ) . ' subscriptions before notification ' );
580+
581+ $ post_data = array_merge ([
582+ 'post_time ' => 1349413322 ,
583+ 'poster_id ' => 1 ,
584+ 'topic_title ' => '' ,
585+ 'post_subject ' => '' ,
586+ 'post_username ' => '' ,
587+ 'forum_name ' => '' ,
588+ ], $ post_data );
589+
590+ // Send notifications, which should trigger cleanup of expired subscription
591+ $ this ->notifications ->add_notifications ($ notification_type , $ post_data );
592+
593+ // Count subscriptions after notification - expired one should be deleted
594+ $ subscriptions_after = $ this ->get_subscription_count ();
595+ $ this ->assertEquals (count ($ expected_users ) - 1 , $ subscriptions_after , 'Expected expired subscription to be deleted ' );
596+
597+ // Verify the expired subscription is actually gone
598+ $ remaining_subs = $ this ->get_all_subscriptions ();
599+ foreach ($ remaining_subs as $ sub )
600+ {
601+ $ this ->assertNotEquals ($ first_user_sub ['endpoint ' ], $ sub ['endpoint ' ], 'Expired subscription should be deleted ' );
602+ }
603+ }
604+
554605 public function test_get_type (): void
555606 {
556607 $ this ->assertEquals ('notification.method.phpbb.wpn.webpush ' , $ this ->notification_method_webpush ->get_type ());
557608 }
558609
610+ /**
611+ * Test is_subscription_unauthorized method with various HTTP status codes
612+ */
613+ public function test_is_subscription_unauthorized (): void
614+ {
615+ $ reflection = new \ReflectionMethod ($ this ->notification_method_webpush , 'is_subscription_unauthorized ' );
616+ $ reflection ->setAccessible (true );
617+
618+ // Test 401 status (should return true)
619+ $ response_401 = $ this ->createMockResponse (401 );
620+ $ request_401 = $ this ->createMockRequest ();
621+ $ report_401 = new \Minishlink \WebPush \MessageSentReport ($ request_401 , $ response_401 , false , 'Unauthorized ' );
622+ $ this ->assertTrue ($ reflection ->invoke ($ this ->notification_method_webpush , $ report_401 ), 'Expected 401 to be treated as unauthorized ' );
623+
624+ // Test 404 status (should return false, handled by isSubscriptionExpired)
625+ $ response_404 = $ this ->createMockResponse (404 );
626+ $ request_404 = $ this ->createMockRequest ();
627+ $ report_404 = new \Minishlink \WebPush \MessageSentReport ($ request_404 , $ response_404 , false , 'Not Found ' );
628+ $ this ->assertFalse ($ reflection ->invoke ($ this ->notification_method_webpush , $ report_404 ), 'Expected 404 to not be treated as unauthorized ' );
629+
630+ // Test 410 status (should return false, handled by isSubscriptionExpired)
631+ $ response_410 = $ this ->createMockResponse (410 );
632+ $ request_410 = $ this ->createMockRequest ();
633+ $ report_410 = new \Minishlink \WebPush \MessageSentReport ($ request_410 , $ response_410 , false , 'Gone ' );
634+ $ this ->assertFalse ($ reflection ->invoke ($ this ->notification_method_webpush , $ report_410 ), 'Expected 410 to not be treated as unauthorized ' );
635+
636+ // Test 429 status (should return false, temporary error)
637+ $ response_429 = $ this ->createMockResponse (429 );
638+ $ request_429 = $ this ->createMockRequest ();
639+ $ report_429 = new \Minishlink \WebPush \MessageSentReport ($ request_429 , $ response_429 , false , 'Too Many Requests ' );
640+ $ this ->assertFalse ($ reflection ->invoke ($ this ->notification_method_webpush , $ report_429 ), 'Expected 429 to not be treated as unauthorized ' );
641+
642+ // Test 500 status (should return false, temporary error)
643+ $ response_500 = $ this ->createMockResponse (500 );
644+ $ request_500 = $ this ->createMockRequest ();
645+ $ report_500 = new \Minishlink \WebPush \MessageSentReport ($ request_500 , $ response_500 , false , 'Internal Server Error ' );
646+ $ this ->assertFalse ($ reflection ->invoke ($ this ->notification_method_webpush , $ report_500 ), 'Expected 500 to not be treated as unauthorized ' );
647+
648+ // Test null response (network failure - should return false)
649+ $ request_null = $ this ->createMockRequest ();
650+ $ report_null = new \Minishlink \WebPush \MessageSentReport ($ request_null , null , false , 'Network error ' );
651+ $ this ->assertFalse ($ reflection ->invoke ($ this ->notification_method_webpush , $ report_null ), 'Expected null response to not be treated as unauthorized ' );
652+ }
653+
654+ /**
655+ * Create a mock PSR-7 ResponseInterface with specified status code
656+ */
657+ protected function createMockResponse (int $ status_code ): \Psr \Http \Message \ResponseInterface
658+ {
659+ $ response = $ this ->getMockBuilder (\Psr \Http \Message \ResponseInterface::class)
660+ ->getMock ();
661+ $ response ->method ('getStatusCode ' )
662+ ->willReturn ($ status_code );
663+ return $ response ;
664+ }
665+
666+ /**
667+ * Create a mock PSR-7 RequestInterface
668+ */
669+ protected function createMockRequest (): \Psr \Http \Message \RequestInterface
670+ {
671+ $ uri = $ this ->getMockBuilder (\Psr \Http \Message \UriInterface::class)
672+ ->getMock ();
673+ $ uri ->method ('__toString ' )
674+ ->willReturn ('http://localhost:9012/notify/test ' );
675+
676+ $ request = $ this ->getMockBuilder (\Psr \Http \Message \RequestInterface::class)
677+ ->getMock ();
678+ $ request ->method ('getUri ' )
679+ ->willReturn ($ uri );
680+
681+ $ body = $ this ->getMockBuilder (\Psr \Http \Message \StreamInterface::class)
682+ ->getMock ();
683+ $ body ->method ('getContents ' )
684+ ->willReturn ('test payload ' );
685+ $ request ->method ('getBody ' )
686+ ->willReturn ($ body );
687+
688+ return $ request ;
689+ }
690+
559691 /**
560692 * @dataProvider data_notification_webpush
561693 */
@@ -751,6 +883,28 @@ protected function get_notifications(): array
751883 return $ sql_ary ;
752884 }
753885
886+ protected function get_subscription_count (): int
887+ {
888+ $ push_subscriptions_table = $ this ->container ->getParameter ('tables.phpbb.wpn.push_subscriptions ' );
889+ $ sql = 'SELECT COUNT(*) as count FROM ' . $ push_subscriptions_table ;
890+ $ result = $ this ->db ->sql_query ($ sql );
891+ $ count = (int ) $ this ->db ->sql_fetchfield ('count ' );
892+ $ this ->db ->sql_freeresult ($ result );
893+
894+ return $ count ;
895+ }
896+
897+ protected function get_all_subscriptions (): array
898+ {
899+ $ push_subscriptions_table = $ this ->container ->getParameter ('tables.phpbb.wpn.push_subscriptions ' );
900+ $ sql = 'SELECT * FROM ' . $ push_subscriptions_table ;
901+ $ result = $ this ->db ->sql_query ($ sql );
902+ $ sql_ary = $ this ->db ->sql_fetchrowset ($ result );
903+ $ this ->db ->sql_freeresult ($ result );
904+
905+ return $ sql_ary ;
906+ }
907+
754908 /**
755909 * @depends test_get_subscription
756910 */
0 commit comments