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
100 changes: 99 additions & 1 deletion tests/integration/TestAPIIntegration.php
Comment thread
sherwinski marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ protected function setUpContentFiltering() {
public function setUp(): void {
parent::setUp();

// Reset HTTP mocks
// Reset HTTP mocks and captured args
self::$http_requests_mock = array();
self::$captured_request_args = array();

global $wp_post_meta, $test_get_option_overrides;
$wp_post_meta = array();
Expand Down Expand Up @@ -736,6 +737,103 @@ public function test_store_notice_error_status_on_wp_error() {
$this->assertSame('Connection timeout', $this->lastTransient['detail']);
}

/**
* Test that a pending notification is cancelled when a scheduled post reverts to draft.
*/
public function test_cancel_notification_when_scheduled_post_reverts_to_draft() {
$post_id = 2001;
$notification_id = 'scheduled-notif-to-cancel';

global $wp_post_meta;
$wp_post_meta[$post_id]['os_notification_id'] = $notification_id;
$wp_post_meta[$post_id]['os_previous_publish_date'] = '2030-06-01 10:00:00';

WP_Mock::userFunction('delete_post_meta')
->andReturnUsing(function($pid, $meta_key) {
global $wp_post_meta;
unset($wp_post_meta[$pid][$meta_key]);
return true;
});

$cancel_url = 'https://onesignal.com/api/v1/notifications/' . $notification_id . '?app_id=test-app-id';
$this->mock_http_request($cancel_url, [
'response' => ['code' => 200],
'body' => json_encode(['success' => true]),
]);

$post = (object) [
'ID' => $post_id,
'post_title' => 'Formerly Scheduled Post',
'post_type' => 'post',
'post_date' => '2030-06-01 10:00:00',
'post_date_gmt' => '2030-06-01 10:00:00',
];

onesignal_schedule_notification('draft', 'future', $post);

$this->assertArrayHasKey('wp_remote_request', self::$captured_request_args);
$this->assertArrayHasKey($cancel_url, self::$captured_request_args['wp_remote_request']);
$this->assertSame('', onesignal_get_notification_id($post_id));
$this->assertSame('', get_post_meta($post_id, 'os_previous_publish_date', true));
}

/**
* Test that no cancellation is attempted when a scheduled post reverts to draft but has no stored notification ID.
*/
public function test_no_cancellation_when_no_notification_stored_on_draft_revert() {
$post_id = 2002;

WP_Mock::userFunction('delete_post_meta')
->andReturnUsing(function($pid, $meta_key) {
global $wp_post_meta;
unset($wp_post_meta[$pid][$meta_key]);
return true;
});

$post = (object) [
'ID' => $post_id,
'post_title' => 'No Notification Post',
'post_type' => 'post',
'post_date' => '2030-06-01 10:00:00',
'post_date_gmt' => '2030-06-01 10:00:00',
];

onesignal_schedule_notification('draft', 'future', $post);

// No HTTP request should have been made
$this->assertArrayNotHasKey('wp_remote_request', self::$captured_request_args);
}

/**
* Test that no cancellation is attempted for disallowed post types on draft revert.
*/
public function test_no_cancellation_for_disallowed_post_type_on_draft_revert() {
$post_id = 2003;
$notification_id = 'should-not-be-cancelled';

global $wp_post_meta;
$wp_post_meta[$post_id]['os_notification_id'] = $notification_id;

WP_Mock::userFunction('delete_post_meta')
->andReturnUsing(function($pid, $meta_key) {
global $wp_post_meta;
unset($wp_post_meta[$pid][$meta_key]);
return true;
});

$post = (object) [
'ID' => $post_id,
'post_title' => 'Disallowed Type Post',
'post_type' => 'portfolio',
];

onesignal_schedule_notification('draft', 'future', $post);

// No HTTP request should have been made; notification ID should remain
$this->assertArrayNotHasKey('wp_remote_request', self::$captured_request_args);
$this->assertSame($notification_id, onesignal_get_notification_id($post_id));
}

/**
* Test store_send_notice stores status='error' and the API error string as detail
* when the API returns a non-200 response with a string errors[0].
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/TestPostHooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public function setUp(): void {
'accepted_args' => 3
);
$wp_filters['wp_trash_post'][10][] = array(
'function' => 'onesignal_cancel_notification_on_post_delete',
'function' => 'onesignal_cancel_and_clear_notification',
'accepted_args' => 1
);
}
Expand All @@ -215,7 +215,7 @@ public function test_save_post_hook_registered() {
* Test that wp_trash_post hook is registered
*/
public function test_wp_trash_post_hook_registered() {
$this->assertNotFalse(has_action('wp_trash_post', 'onesignal_cancel_notification_on_post_delete'));
$this->assertNotFalse(has_action('wp_trash_post', 'onesignal_cancel_and_clear_notification'));
}

/**
Expand Down
47 changes: 30 additions & 17 deletions v3/onesignal-notification.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
add_action('save_post', 'onesignal_handle_quick_edit_date_change', 10, 3);

// Register handler to cancel scheduled notifications when posts are deleted
add_action('wp_trash_post', 'onesignal_cancel_notification_on_post_delete');
add_action('wp_trash_post', 'onesignal_cancel_and_clear_notification');

// Display admin notices after a notification send attempt (classic editor)
add_action('admin_notices', 'onesignal_display_send_notice');
Expand All @@ -21,7 +21,9 @@
// Enqueue block editor notice script
add_action('enqueue_block_editor_assets', 'onesignal_enqueue_block_editor_notice');

// Core function to create and send/schedule a notification
/**
* Creates and sends (or schedules) a OneSignal notification for a post.
*/
function onesignal_create_notification($post, $notification_options = array())
{
$onesignal_wp_settings = get_option("OneSignalWPSetting");
Expand Down Expand Up @@ -306,7 +308,9 @@ function onesignal_enqueue_block_editor_notice()
));
}

// Function to schedule notification (called on post status transitions)
/**
* Fires on every post status transition; sends or cancels notifications as appropriate.
*/
function onesignal_schedule_notification($new_status, $old_status, $post)
{
if (($new_status === 'publish') || ($new_status === 'future')) {
Expand All @@ -328,10 +332,20 @@ function onesignal_schedule_notification($new_status, $old_status, $post)

// Call the core notification function
onesignal_create_notification($post, $notification_options);
} elseif ($old_status === 'future') {
// A scheduled post was reverted to a non-scheduled status (e.g. draft, pending).
// Cancel the pending OneSignal notification so it isn't delivered at the old scheduled time.
if (!onesignal_is_post_type_allowed($post->post_type)) {
return;
}

onesignal_cancel_and_clear_notification($post->ID);
}
}

// Function to handle quick-edit publish date changes
/**
* Cancels and re-schedules a notification when a scheduled post's publish date is changed via quick-edit.
*/
function onesignal_handle_quick_edit_date_change($post_id, $post, $update)
{
// Check user capability to edit this post
Expand Down Expand Up @@ -405,21 +419,20 @@ function onesignal_handle_quick_edit_date_change($post_id, $post, $update)
}
}

// Function to cancel scheduled notifications when a post is deleted
function onesignal_cancel_notification_on_post_delete($post_id)
/**
* Cancels the stored OneSignal notification, and clears its meta, on post deletion or status change (e.g. scheduled -> draft).
*/
function onesignal_cancel_and_clear_notification($post_id)
{
$post = get_post($post_id);

if (!$post) {
return;
$existing_notification_id = onesignal_get_notification_id($post_id);
if (empty($existing_notification_id)) {
return false;
}

$existing_notification_id = onesignal_get_notification_id($post_id);
if (!empty($existing_notification_id)) {
$cancelled = onesignal_cancel_notification($existing_notification_id);
if ($cancelled) {
delete_post_meta($post_id, 'os_notification_id');
delete_post_meta($post_id, 'os_previous_publish_date');
}
$cancelled = onesignal_cancel_notification($existing_notification_id);
if ($cancelled) {
delete_post_meta($post_id, 'os_notification_id');
delete_post_meta($post_id, 'os_previous_publish_date');
}
return $cancelled;
}
Loading