blob: ba5716c895b0fb090ddc2a464f16c30e79f35999 [file] [log] [blame] [edit]
/*
* Copyright (C) 2021-2023 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "DaemonTestUtilities.h"
#import "HTTPServer.h"
#import "MessageSenderInlines.h"
#import "PlatformUtilities.h"
#import "PushClientConnectionMessages.h"
#import "PushMessageForTesting.h"
#import "Test.h"
#import "TestNavigationDelegate.h"
#import "TestNotificationProvider.h"
#import "TestURLSchemeHandler.h"
#import "TestWKWebView.h"
#import "Utilities.h"
#import "WebPushDaemonConnectionConfiguration.h"
#import <WebCore/PushSubscriptionIdentifier.h>
#import <WebCore/SecurityOriginData.h>
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKProcessPoolPrivate.h>
#import <WebKit/WKUIDelegatePrivate.h>
#import <WebKit/WKWebsiteDataRecordPrivate.h>
#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/WebPushDaemonConstants.h>
#import <WebKit/_WKFeature.h>
#import <WebKit/_WKNotificationData.h>
#import <WebKit/_WKProcessPoolConfiguration.h>
#import <WebKit/_WKWebPushDaemonConnection.h>
#import <WebKit/_WKWebPushSubscriptionData.h>
#import <WebKit/_WKWebsiteDataStoreConfiguration.h>
#import <WebKit/_WKWebsiteDataStoreDelegate.h>
#import <mach/mach_init.h>
#import <mach/task.h>
#import <ranges>
#import <wtf/BlockPtr.h>
#import <wtf/OSObjectPtr.h>
#import <wtf/StdLibExtras.h>
#import <wtf/UUID.h>
#import <wtf/UniqueRef.h>
#import <wtf/cocoa/SpanCocoa.h>
#import <wtf/darwin/DispatchExtras.h>
#import <wtf/darwin/XPCExtras.h>
#import <wtf/darwin/XPCObjectPtr.h>
#import <wtf/text/Base64.h>
#import <wtf/text/MakeString.h>
// FIXME: Work through enabling on iOS
#if ENABLE(NOTIFICATIONS) && ENABLE(NOTIFICATION_EVENT) && PLATFORM(MAC)
static bool alertReceived = false;
@interface PushNotificationDelegate : NSObject<WKUIDelegatePrivate, _WKWebsiteDataStoreDelegate> {
RetainPtr<_WKNotificationData> _mostRecentNotification;
RetainPtr<NSURL> _mostRecentActionURL;
std::optional<uint64_t> _mostRecentAppBadge;
}
-(void)clearMostRecents;
@property BOOL expectsDelegateNotificationCallbacks;
@property (nonatomic, readonly) RetainPtr<_WKNotificationData> mostRecentNotification;
@property (nonatomic, readonly) RetainPtr<NSURL> mostRecentActionURL;
@property (nonatomic, readonly) std::optional<uint64_t> mostRecentAppBadge;
@end
@implementation PushNotificationDelegate
-(id)init {
if (self = [super init])
self.expectsDelegateNotificationCallbacks = YES;
return self;
}
-(void)clearMostRecents
{
_mostRecentNotification = nullptr;
_mostRecentActionURL = nullptr;
_mostRecentAppBadge = std::nullopt;
}
- (void)_webView:(WKWebView *)webView requestNotificationPermissionForSecurityOrigin:(WKSecurityOrigin *)securityOrigin decisionHandler:(void (^)(BOOL))decisionHandler
{
decisionHandler(true);
}
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
alertReceived = true;
completionHandler();
}
- (void)websiteDataStore:(WKWebsiteDataStore *)dataStore showNotification:(_WKNotificationData *)notificationData
{
RELEASE_ASSERT(_expectsDelegateNotificationCallbacks);
_mostRecentNotification = notificationData;
}
- (void)websiteDataStore:(WKWebsiteDataStore *)dataStore workerOrigin:(WKSecurityOrigin *)workerOrigin updatedAppBadge:(NSNumber *)badge
{
if (badge)
_mostRecentAppBadge = [badge unsignedLongLongValue];
else
_mostRecentAppBadge = std::nullopt;
}
- (void)websiteDataStore:(WKWebsiteDataStore *)dataStore navigateToNotificationActionURL:(NSURL *)url
{
_mostRecentActionURL = url;
}
@end
#if ASSERT_ENABLED
namespace WTF {
template<> bool isValidEnum<WebKit::WebPushD::PushMessageDisposition>(std::underlying_type_t<WebKit::WebPushD::PushMessageDisposition>) { return true; }
template<> bool isValidEnum<WebCore::NotificationDirection>(std::underlying_type_t<WebCore::NotificationDirection>) { return true; }
}
#endif
namespace TestWebKitAPI {
static bool done;
static RetainPtr<NSURL> testWebPushDaemonLocation()
{
return [currentExecutableDirectory() URLByAppendingPathComponent:@"webpushd" isDirectory:NO];
}
enum LaunchOnlyOnce : BOOL { No, Yes };
enum class InstallDataStoreDelegate : bool { No, Yes };
enum class BuiltInNotificationsEnabled : bool { No, Yes };
static NSDictionary<NSString *, id> *testWebPushDaemonPList(NSURL *storageLocation, LaunchOnlyOnce launchOnlyOnce)
{
return @{
@"Label" : @"org.webkit.webpushtestdaemon",
@"LaunchOnlyOnce" : @(static_cast<BOOL>(launchOnlyOnce)),
@"ThrottleInterval" : @(1),
@"StandardErrorPath" : [storageLocation URLByAppendingPathComponent:@"daemon_stderr"].path,
@"EnvironmentVariables" : @{ @"DYLD_FRAMEWORK_PATH" : currentExecutableDirectory().get().path },
@"MachServices" : @{ @"org.webkit.webpushtestdaemon.service" : @YES },
@"ProgramArguments" : @[
testWebPushDaemonLocation().get().path,
@"--machServiceName",
@"org.webkit.webpushtestdaemon.service",
@"--useMockPushService"
]
};
}
static bool shouldSetupWebPushD()
{
static bool shouldSetup = true;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray<NSString *> *arguments = [[NSProcessInfo processInfo] arguments];
if ([arguments containsObject:@"--no-webpushd"])
shouldSetup = false;
});
return shouldSetup;
}
static NSURL *setUpTestWebPushD(LaunchOnlyOnce launchOnlyOnce = LaunchOnlyOnce::Yes)
{
if (!shouldSetupWebPushD())
return nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *tempDir = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"WebPushDaemonTest"] isDirectory:YES];
NSError *error = nil;
if ([fileManager fileExistsAtPath:tempDir.path])
[fileManager removeItemAtURL:tempDir error:&error];
EXPECT_NULL(error);
killFirstInstanceOfDaemon(@"webpushd");
registerPlistWithLaunchD(testWebPushDaemonPList(tempDir, launchOnlyOnce), tempDir);
return tempDir;
}
// Only works if the test daemon was registered with LaunchOnlyOnce::No.
static BOOL restartTestWebPushD()
{
return restartService(@"org.webkit.webpushtestdaemon", @"webpushd");
}
static void cleanUpTestWebPushD(NSURL *tempDir)
{
if (!shouldSetupWebPushD())
return;
killFirstInstanceOfDaemon(@"webpushd");
if (![[NSFileManager defaultManager] fileExistsAtPath:tempDir.path])
return;
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtURL:tempDir error:&error];
if (error)
NSLog(@"Error removing tempDir URL: %@", error);
EXPECT_NULL(error);
}
template<typename T, typename = void> struct TestArgumentCoder;
template<typename T> struct TestArgumentCoder<T, typename std::enable_if_t<std::is_enum_v<T>>> : public IPC::ArgumentCoder<T> { };
template<typename T> struct TestArgumentCoder<Vector<T>> : public IPC::ArgumentCoder<Vector<T>> { };
template<typename T> struct TestArgumentCoder<std::span<T>> : public IPC::ArgumentCoder<std::span<T>> { };
template<typename T> struct TestArgumentCoder<std::optional<T>> : public IPC::ArgumentCoder<std::optional<T>> { };
template<typename... Elements> struct TestArgumentCoder<std::tuple<Elements...>> : public IPC::ArgumentCoder<std::tuple<Elements...>> { };
template<typename T> struct TestArgumentCoder<T, typename std::enable_if_t<std::is_arithmetic_v<T>>> {
template<typename Encoder> static void encode(Encoder& encoder, T value) { encoder.encodeInteger(value); }
template<typename Decoder> static std::optional<T> decode(Decoder& decoder) { return decoder.template decodeInteger<T>(); }
};
template<> struct TestArgumentCoder<String> {
template<typename Encoder>
static void encode(Encoder& encoder, const String& string)
{
if (string.isNull()) {
encoder << std::numeric_limits<uint32_t>::max();
return;
}
bool is8Bit = string.is8Bit();
encoder << string.length();
encoder << is8Bit;
if (is8Bit)
encoder.encodeSpan(string.span8());
else
encoder.encodeSpan(string.span16());
}
template<typename CharacterType, typename Decoder>
static std::optional<String> decodeStringText(Decoder& decoder, uint32_t length)
{
Vector<CharacterType> characters;
for (size_t i = 0; i < length; i++) {
auto character = decoder.template decodeInteger<CharacterType>();
if (!character)
return std::nullopt;
characters.append(*character);
}
return String(characters.span());
}
template<typename Decoder>
static std::optional<String> decode(Decoder& decoder)
{
auto length = decoder.template decode<uint32_t>();
if (!length)
return std::nullopt;
if (*length == std::numeric_limits<uint32_t>::max())
return String();
auto is8Bit = decoder.template decode<bool>();
if (!is8Bit)
return std::nullopt;
if (*is8Bit)
return decodeStringText<Latin1Character>(decoder, *length);
return decodeStringText<char16_t>(decoder, *length);
}
};
template<> struct TestArgumentCoder<WTF::UUID> {
template<typename Encoder> static void encode(Encoder& encoder, const WTF::UUID& uuid)
{
encoder << uuid.high();
encoder << uuid.low();
}
};
template<> struct TestArgumentCoder<URL> {
template<typename Encoder> static void encode(Encoder& encoder, const URL& url) { encoder << url.string(); }
template<typename Decoder> static std::optional<URL> decode(Decoder& decoder)
{
auto string = decoder.template decode<String>();
if (!string)
return std::nullopt;
return { URL(WTF::move(*string)) };
}
};
class TestEncoder {
public:
template<typename T> void encodeHeader()
{
*this << uint8_t();
*this << T::name();
*this << uint64_t();
}
Vector<uint8_t> takeBytes() { return std::exchange(m_bytes, { }); }
template<typename T> TestEncoder& operator<<(T&& t)
{
TestArgumentCoder<std::remove_cvref_t<T>>::encode(*this, std::forward<T>(t));
return *this;
}
template<typename T, size_t Extent> void encodeSpan(std::span<T, Extent> span)
{
while (m_bytes.size() % alignof(T))
m_bytes.append(0);
for (auto byte : asBytes(span))
m_bytes.append(byte);
}
template<typename T> void encodeInteger(T object)
{
encodeSpan(singleElementSpan(object));
}
private:
Vector<uint8_t> m_bytes;
};
class TestDecoder {
public:
TestDecoder(std::span<const uint8_t> buffer)
: m_buffer(buffer)
, m_bufferPosition(m_buffer.begin()) { }
template<typename T> void ignoreHeader()
{
decode<uint8_t>();
RELEASE_ASSERT(decode<IPC::MessageName>() == T::asyncMessageReplyName());
decode<uint64_t>();
}
template<typename T> std::optional<T> decode() { return TestArgumentCoder<std::remove_cvref_t<T>>::decode(*this); }
template<typename T> std::optional<T> decodeInteger()
{
while (m_bufferPosition != m_buffer.end() && bufferOffset() % alignof(T))
m_bufferPosition++;
if (bufferOffset() + sizeof(T) > m_buffer.size())
return std::nullopt;
T value = *reinterpret_cast<const T*>(m_buffer.data() + bufferOffset());
m_bufferPosition += sizeof(T);
return value;
}
private:
size_t bufferOffset() const { return std::distance(m_buffer.begin(), m_bufferPosition); }
std::span<const uint8_t> m_buffer;
std::span<const uint8_t>::iterator m_bufferPosition;
};
template<> struct TestArgumentCoder<WebKit::WebPushD::WebPushDaemonConnectionConfiguration> {
static void encode(TestEncoder& encoder, const WebKit::WebPushD::WebPushDaemonConnectionConfiguration& configuration)
{
encoder << configuration.hostAppAuditTokenData;
encoder << configuration.bundleIdentifierOverride;
encoder << configuration.pushPartitionString;
encoder << configuration.dataStoreIdentifier;
encoder << configuration.declarativeWebPushEnabled;
}
};
template<> struct TestArgumentCoder<WebKit::WebPushD::PushMessageForTesting> {
static void encode(TestEncoder& encoder, const WebKit::WebPushD::PushMessageForTesting& message)
{
encoder << message.targetAppCodeSigningIdentifier;
encoder << message.pushPartitionString;
encoder << message.registrationURL;
encoder << message.payload;
encoder << message.disposition;
#if ENABLE(DECLARATIVE_WEB_PUSH)
encoder << message.parsedPayload;
#endif
}
};
template<> struct TestArgumentCoder<WebCore::NotificationPayload> {
static void encode(TestEncoder& encoder, const WebCore::NotificationPayload& payload)
{
encoder << payload.defaultActionURL;
encoder << payload.title;
encoder << payload.appBadge;
encoder << payload.options;
encoder << payload.isMutable;
}
};
template<> struct TestArgumentCoder<WebCore::NotificationOptionsPayload> {
static void encode(TestEncoder& encoder, const WebCore::NotificationOptionsPayload& payload)
{
encoder << payload.dir;
encoder << payload.lang;
encoder << payload.body;
encoder << payload.tag;
encoder << payload.icon;
encoder << payload.dataJSONString;
encoder << payload.silent;
}
};
class WebPushXPCConnectionMessageSender {
public:
WebPushXPCConnectionMessageSender(xpc_connection_t connection)
: m_connection(connection) { }
void setShouldIncrementProtocolVersionForTesting() { m_shouldIncrementProtocolVersionForTesting = true; }
template<typename M>
void sendWithoutUsingIPCConnection(M&&) const;
template<typename M, typename CH>
void sendWithAsyncReplyWithoutUsingIPCConnection(M&&, CH&&) const;
private:
XPCObjectPtr<xpc_object_t> messageDictionaryFromEncoder(TestEncoder&&) const;
XPCObjectPtr<xpc_connection_t> m_connection;
bool m_shouldIncrementProtocolVersionForTesting { false };
};
XPCObjectPtr<xpc_object_t> WebPushXPCConnectionMessageSender::messageDictionaryFromEncoder(TestEncoder&& encoder) const
{
// FIXME: This is a false positive. <rdar://164843889>
SUPPRESS_RETAINPTR_CTOR_ADOPT auto dictionary = adoptXPCObject(xpc_dictionary_create(nullptr, nullptr, 0));
uint64_t protocolVersion = WebKit::WebPushD::protocolVersionValue;
if (m_shouldIncrementProtocolVersionForTesting)
++protocolVersion;
xpc_dictionary_set_uint64(dictionary.get(), WebKit::WebPushD::protocolVersionKey, protocolVersion);
__block auto blockBytes = encoder.takeBytes();
auto buffer = blockBytes.span();
auto dispatchData = adoptOSObject(dispatch_data_create(buffer.data(), buffer.size(), mainDispatchQueueSingleton(), ^{
blockBytes.clear();
}));
// FIXME: This is a false positive. <rdar://164843889>
SUPPRESS_RETAINPTR_CTOR_ADOPT auto encoderData = adoptXPCObject(xpc_data_create_with_dispatch_data(dispatchData.get()));
xpc_dictionary_set_value(dictionary.get(), WebKit::WebPushD::protocolEncodedMessageKey, encoderData.get());
return dictionary;
}
template<typename M>
void WebPushXPCConnectionMessageSender::sendWithoutUsingIPCConnection(M&& message) const
{
TestEncoder encoder;
encoder.encodeHeader<M>();
message.encode(encoder);
auto dictionary = messageDictionaryFromEncoder(WTF::move(encoder));
xpc_connection_send_message(m_connection.get(), dictionary.get());
}
template<typename M, typename CH>
void WebPushXPCConnectionMessageSender::sendWithAsyncReplyWithoutUsingIPCConnection(M&& message, CH&& completionHandler) const
{
TestEncoder encoder;
encoder.encodeHeader<M>();
message.encode(encoder);
auto dictionary = messageDictionaryFromEncoder(WTF::move(encoder));
xpc_connection_send_message_with_reply(m_connection.get(), dictionary.get(), mainDispatchQueueSingleton(), makeBlockPtr([this, completionHandler = WTF::move(completionHandler)] (xpc_object_t reply) mutable {
if (xpc_get_type(reply) == XPC_TYPE_ERROR) {
// We only expect an error if we were purposefully testing the wrong protocol version.
RELEASE_ASSERT(m_shouldIncrementProtocolVersionForTesting);
return IPC::cancelReplyWithoutUsingConnection<M>(WTF::move(completionHandler));
}
if (xpc_get_type(reply) != XPC_TYPE_DICTIONARY)
RELEASE_ASSERT_NOT_REACHED();
if (xpc_dictionary_get_uint64(reply, WebKit::WebPushD::protocolVersionKey) != WebKit::WebPushD::protocolVersionValue)
RELEASE_ASSERT_NOT_REACHED();
auto data = xpcDictionaryGetData(reply, WebKit::WebPushD::protocolEncodedMessageKey);
TestDecoder decoder(data);
decoder.ignoreHeader<M>();
IPC::callReplyWithoutUsingConnection<M>(decoder, WTF::move(completionHandler));
}).get());
}
static audit_token_t getSelfAuditToken()
{
audit_token_t auditToken { };
mach_msg_type_number_t auditTokenCount = TASK_AUDIT_TOKEN_COUNT;
task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)(&auditToken), &auditTokenCount);
return auditToken;
}
static WebKit::WebPushD::WebPushDaemonConnectionConfiguration defaultWebPushDaemonConfiguration()
{
auto token = getSelfAuditToken();
Vector<uint8_t> auditToken(sizeof(token));
memcpySpan(auditToken.mutableSpan(), asByteSpan(token));
IGNORE_CLANG_WARNINGS_BEGIN("missing-designated-field-initializers")
return { .hostAppAuditTokenData = WTF::move(auditToken) };
IGNORE_CLANG_WARNINGS_END
}
XPCObjectPtr<xpc_connection_t> createAndConfigureConnectionToService(const char* serviceName, std::optional<WebKit::WebPushD::WebPushDaemonConnectionConfiguration> configuration = std::nullopt)
{
// FIXME: This is a false positive. <rdar://164843889>
SUPPRESS_RETAINPTR_CTOR_ADOPT auto connection = adoptXPCObject(xpc_connection_create_mach_service(serviceName, mainDispatchQueueSingleton(), 0));
xpc_connection_set_event_handler(connection.get(), ^(xpc_object_t) { });
xpc_connection_activate(connection.get());
auto sender = WebPushXPCConnectionMessageSender { connection.get() };
if (!configuration)
configuration = defaultWebPushDaemonConfiguration();
sender.sendWithoutUsingIPCConnection(Messages::PushClientConnection::InitializeConnection(configuration.value()));
return connection;
}
TEST(WebPushD, BasicCommunication)
{
NSURL *tempDir = setUpTestWebPushD();
// FIXME: This is a false positive. <rdar://164843889>
SUPPRESS_RETAINPTR_CTOR_ADOPT auto connection = adoptXPCObject(xpc_connection_create_mach_service("org.webkit.webpushtestdaemon.service", mainDispatchQueueSingleton(), 0));
__block bool done = false;
__block bool interrupted = false;
xpc_connection_set_event_handler(connection.get(), ^(xpc_object_t request) {
if (request == XPC_ERROR_CONNECTION_INTERRUPTED) {
interrupted = true;
return;
}
});
xpc_connection_activate(connection.get());
// Send a basic message and make sure its reply handler ran.
auto sender = WebPushXPCConnectionMessageSender { connection.get() };
sender.sendWithoutUsingIPCConnection(Messages::PushClientConnection::InitializeConnection(defaultWebPushDaemonConfiguration()));
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::GetPushTopicsForTesting(), ^(Vector<String>, Vector<String>) {
done = true;
});
TestWebKitAPI::Util::run(&done);
// Sending a message with a higher protocol version should cause the connection to be terminated.
sender.setShouldIncrementProtocolVersionForTesting();
__block bool messageReplied = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::RemoveAllPushSubscriptions(), ^(bool result) {
EXPECT_FALSE(result);
messageReplied = true;
});
TestWebKitAPI::Util::run(&messageReplied);
TestWebKitAPI::Util::run(&interrupted);
cleanUpTestWebPushD(tempDir);
}
static void clearWebsiteDataStore(WKWebsiteDataStore *store)
{
__block bool clearedStore = false;
[store removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() {
[store _clearResourceLoadStatistics:^{
clearedStore = true;
}];
}];
TestWebKitAPI::Util::run(&clearedStore);
}
static ASCIILiteral validServerKey = "BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs"_s;
static ASCIILiteral keyThatCausesInjectedFailure = "BEAxaUMo1s8tjORxJfnSSvWhYb4u51kg1hWT2s_9gpV7Zxar1pF_2BQ8AncuAdS2BoLhN4qaxzBy2CwHE8BBzWg"_s;
static constexpr auto navigatorHTMLSource = R"SRC(
<script>
let globalSubscription = null;
function log(msg)
{
window.webkit.messageHandlers.test.postMessage(msg);
}
window.onload = function()
{
log("Ready");
}
async function subscribe(key)
{
try {
globalSubscription = await window.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: key
});
return globalSubscription.toJSON();
} catch (error) {
return "Error: " + error;
}
}
async function unsubscribe()
{
try {
let result = await globalSubscription.unsubscribe();
return result;
} catch (error) {
return "Error: " + error;
}
}
async function getPushSubscription()
{
try {
let subscription = await window.pushManager.getSubscription();
return subscription ? subscription.toJSON() : null;
} catch (error) {
return "Error: " + error;
}
}
</script>
)SRC"_s;
static constexpr auto htmlSource = R"SRC(
<script>
let globalRegistration = null;
let globalSubscription = null;
function log(msg)
{
window.webkit.messageHandlers.test.postMessage(msg);
}
const channel = new MessageChannel();
channel.port1.onmessage = (event) => log(event.data);
navigator.serviceWorker.register('/sw.js').then(async () => {
globalRegistration = await navigator.serviceWorker.ready;
globalRegistration.active.postMessage({ message: "setup", port: channel.port2 }, [channel.port2]);
}).catch(function(error) {
log("Registration failed with: " + error);
});
async function subscribe(key)
{
try {
globalSubscription = await globalRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: key
});
return globalSubscription.toJSON();
} catch (error) {
return "Error: " + error;
}
}
async function unsubscribe()
{
try {
let result = await globalSubscription.unsubscribe();
return result;
} catch (error) {
return "Error: " + error;
}
}
async function getNotificationPermissionFromServiceWorker()
{
const channel = new MessageChannel();
const promise = new Promise((resolve) => {
channel.port1.onmessage = (event) => resolve(event.data);
});
globalRegistration.active.postMessage({ message: "notificationPermission", port: channel.port2 }, [channel.port2]);
return await promise;
}
async function getPushPermissionState()
{
try {
return await globalRegistration.pushManager.permissionState();
} catch (error) {
return "Error: " + error;
}
}
async function getPushPermissionStateFromServiceWorker()
{
const channel = new MessageChannel();
const promise = new Promise((resolve) => {
channel.port1.onmessage = (event) => resolve(event.data);
});
globalRegistration.active.postMessage({ message: "getPushPermissionState", port: channel.port2 }, [channel.port2]);
return await promise;
}
async function queryPermission(name)
{
try {
let status = await navigator.permissions.query({ name });
return status.state;
} catch (error) {
return "Error: " + error;
}
}
async function queryPermissionFromServiceWorker(name)
{
const channel = new MessageChannel();
const promise = new Promise((resolve) => {
channel.port1.onmessage = (event) => resolve(event.data);
});
globalRegistration.active.postMessage({ message: "queryPermission", arguments: [name], port: channel.port2 }, [channel.port2]);
return await promise;
}
async function getPushSubscriptionFromWindow()
{
try {
let subscription = await window.pushManager.getSubscription();
return subscription;
} catch (error) {
return "Error: " + error;
}
}
async function getPushSubscription()
{
try {
let subscription = await globalRegistration.pushManager.getSubscription();
return subscription ? subscription.toJSON() : null;
} catch (error) {
try {
let subscription = await getPushSubscriptionFromWindow();
return subscription ? subscription.toJSON() : null;
} catch (error2) {
return "Error(s): " + error + ", " + error2;
}
}
}
async function disableShowNotifications()
{
const channel = new MessageChannel();
const promise = new Promise((resolve) => {
channel.port1.onmessage = (event) => resolve(event.data);
});
globalRegistration.active.postMessage({ message: "disableShowNotifications", port: channel.port2 }, [channel.port2]);
return await promise;
}
async function getNotifications()
{
const channel = new MessageChannel();
const promise = new Promise((resolve) => {
channel.port1.onmessage = (event) => resolve(event.data);
});
globalRegistration.active.postMessage({ message: "getNotifications", port: channel.port2 }, [channel.port2]);
return await promise;
}
async function closeAllNotifications()
{
const channel = new MessageChannel();
const promise = new Promise((resolve) => {
channel.port1.onmessage = (event) => resolve(event.data);
});
globalRegistration.active.postMessage({ message: "closeAllNotifications", port: channel.port2 }, [channel.port2]);
return await promise;
}
</script>
)SRC"_s;
static constexpr auto serviceWorkerScriptSource = R"SWRESOURCE(
let globalPort;
let showNotifications = true;
function notificationToString(n)
{
return "title: " + n.title + " body: " + n.body + " tag: " + n.tag + " dir: " + n.dir + " silent: " + n.silent + " data: " + n.data;
}
self.addEventListener("message", (event) => {
let { message, arguments, port } = event.data;
var closeAllNotifications = message === "closeAllNotifications";
if (message === "setup") {
globalPort = port;
port.postMessage("Ready");
} else if (message === "notificationPermission") {
port.postMessage(Notification.permission);
} else if (message === "getPushPermissionState") {
registration.pushManager.permissionState().then(port.postMessage.bind(port), (error) => {
port.postMessage("getPushPermissionState failed: " + error);
});
} else if (message === "queryPermission") {
let [name] = arguments;
navigator.permissions.query({ name }).then((status) => {
port.postMessage(status.state);
}, (error) => {
port.postMessage("queryPermission failed: " + error);
});
} else if (message === "disableShowNotifications") {
showNotifications = false;
port.postMessage(true);
} else if (message === "getNotifications" || closeAllNotifications) {
registration.getNotifications().then((notifications) => {
var result = "";
for (n = 0; n < notifications.length; ++n) {
result += n + " - " + notificationToString(notifications[n]) + " ";
if (closeAllNotifications)
notifications[n].close();
}
port.postMessage(result);
}, (exception) => {
port.postMessage("getNotifications failed: " + exception);
});
}
});
self.addEventListener("push", async (event) => {
if (event.notification) {
// If the tag is empty, do nothing
if (!event.notification.tag)
return;
var optionsFromTag = event.notification.tag.split(" ");
var newTitle;
var newBadge;
var newActionURL;
if (optionsFromTag[0] == "titleandbadge") {
newTitle = optionsFromTag[1];
newBadge = optionsFromTag[2];
} else if (optionsFromTag[0] == "title")
newTitle = optionsFromTag[1];
else if (optionsFromTag[0] == "badge")
newBadge = optionsFromTag[1];
else if (optionsFromTag[0] == "datatotitle")
newTitle = event.notification.data;
else if (optionsFromTag[0] == "defaultactionurl")
newActionURL = optionsFromTag[1];
else if (optionsFromTag[0] == "emptydefaultactionurl") {
self.registration.showNotification("Missing default action").then((value) => {
globalPort.postMessage("showNotification succeeded");
}, (exception) => {
globalPort.postMessage("showNotification failed: " + exception);
});
}
if (newTitle || newActionURL) {
if (!newTitle)
newTitle = event.notification.title;
if (!newActionURL)
newActionURL = event.notification.navigate;
self.registration.showNotification(newTitle, { "navigate": newActionURL });
}
if (newBadge)
navigator.setAppBadge(newBadge);
return;
}
try {
if (showNotifications) {
await self.registration.showNotification("notification");
navigator.setAppBadge(42);
}
if (!event.data) {
globalPort.postMessage("Received: null data");
return;
}
const value = event.data.text();
globalPort.postMessage("Received: " + value);
} catch (e) {
globalPort.postMessage("Error: " + e);
}
});
self.addEventListener("notificationclick", () => {
globalPort.postMessage("Received: notificationclick");
});
)SWRESOURCE"_s;
static void enableFeatureForPreferences(NSString *featureName, WKPreferences *preferences)
{
for (_WKFeature *feature in [WKPreferences _features]) {
if ([feature.key isEqualToString:featureName]) {
[preferences _setEnabled:YES forFeature:feature];
break;
}
}
}
class WebPushDTestWebView {
WTF_DEPRECATED_MAKE_FAST_ALLOCATED(WebPushDTestWebView);
public:
WebPushDTestWebView(const String& pushPartition, const std::optional<WTF::UUID>& dataStoreIdentifier, WKProcessPool *processPool, TestNotificationProvider& notificationProvider, ASCIILiteral html, InstallDataStoreDelegate installDataStoreDelegate, BuiltInNotificationsEnabled builtInNotificationsEnabled)
: m_pushPartition(pushPartition)
, m_dataStoreIdentifier(dataStoreIdentifier)
, m_notificationProvider(notificationProvider)
{
m_origin = "https://example.com"_s;
m_url = adoptNS([[NSURL alloc] initWithString:@"https://example.com/"]);
m_testMessageHandler = adoptNS([[TestMessageHandler alloc] init]);
__block bool ready = false;
[m_testMessageHandler addMessage:@"Ready" withHandler:^{
ready = true;
}];
m_server = makeUnique<TestWebKitAPI::HTTPServer>(std::initializer_list<std::pair<String, TestWebKitAPI::HTTPResponse>> {
{ "/"_s, { html } },
{ "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, serviceWorkerScriptSource } }
}, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
// This step is required early to make sure the first NetworkProcess access has the correct
// setting in the NetworkProcessInitializationParameters
if (builtInNotificationsEnabled == BuiltInNotificationsEnabled::Yes)
enableFeatureForPreferences(@"BuiltInNotificationsEnabled", configuration.get().preferences);
RetainPtr<_WKWebsiteDataStoreConfiguration> dataStoreConfiguration;
if (dataStoreIdentifier)
dataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initWithIdentifier:dataStoreIdentifier->createNSUUID().get()]);
else
dataStoreConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]);
[dataStoreConfiguration setWebPushPartitionString:pushPartition.createNSString().get()];
[dataStoreConfiguration setProxyConfiguration:@{
(NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1",
(NSString *)kCFStreamPropertyHTTPSProxyPort: @(m_server->port())
}];
dataStoreConfiguration.get().webPushMachServiceName = @"org.webkit.webpushtestdaemon.service";
#if ENABLE(DECLARATIVE_WEB_PUSH)
dataStoreConfiguration.get().isDeclarativeWebPushEnabled = YES;
#endif
// FIXME: This seems like it shouldn't be necessary, but _clearResourceLoadStatistics (called by clearWebsiteDataStore) doesn't seem to work.
[[NSFileManager defaultManager] removeItemAtURL:[dataStoreConfiguration _resourceLoadStatisticsDirectory] error:nil];
m_dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]);
if (installDataStoreDelegate == InstallDataStoreDelegate::Yes) {
m_delegate = adoptNS([[PushNotificationDelegate alloc] init]);
if (builtInNotificationsEnabled == BuiltInNotificationsEnabled::Yes)
m_delegate.get().expectsDelegateNotificationCallbacks = NO;
m_dataStore.get()._delegate = m_delegate.get();
}
[m_dataStore _setResourceLoadStatisticsEnabled:YES];
clearWebsiteDataStore(m_dataStore.get());
__block bool done = false;
[m_dataStore _setPrevalentDomain:m_url.get() completionHandler:^{
done = true;
}];
Util::run(&done);
done = false;
[m_dataStore _logUserInteraction:m_url.get() completionHandler:^{
done = true;
}];
Util::run(&done);
[configuration setProcessPool:processPool];
[configuration setWebsiteDataStore:m_dataStore.get()];
configuration.get().preferences._appBadgeEnabled = YES;
auto userContentController = [configuration userContentController];
[userContentController addScriptMessageHandler:m_testMessageHandler.get() name:@"test"];
[[configuration preferences] _setPushAPIEnabled:YES];
m_notificationProvider.setPermission(m_origin, true);
#if ENABLE(DECLARATIVE_WEB_PUSH)
enableFeatureForPreferences(@"DeclarativeWebPush", configuration.get().preferences);
#endif
m_webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
[m_webView _setDontResetTransientActivationAfterRunJavaScript:YES];
[m_webView setUIDelegate:m_delegate.get()];
auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
navigationDelegate.get().didReceiveAuthenticationChallenge = ^(WKWebView *, NSURLAuthenticationChallenge *challenge, void (^completionHandler)(NSURLSessionAuthChallengeDisposition, NSURLCredential *)) {
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
};
[m_webView setNavigationDelegate:navigationDelegate.get()];
[m_webView loadRequest:[NSURLRequest requestWithURL:m_url.get()]];
TestWebKitAPI::Util::run(&ready);
}
std::optional<WTF::UUID> dataStoreIdentifier() { return m_dataStoreIdentifier; }
const String& origin() { return m_origin; }
RetainPtr<WKWebsiteDataStore> dataStore() { return m_dataStore; }
id requestNotificationPermission()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await Notification.requestPermission()" withArguments:@{ } error:&error];
return error ?: obj;
}
id subscribe(String key = validServerKey)
{
NSError *error = nil;
auto script = makeString("return await subscribe('"_s, key, "')"_s);
id obj = [m_webView objectByCallingAsyncFunction:script.createNSString().get() withArguments:@{ } error:&error];
return error ?: obj;
}
id unsubscribe()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await unsubscribe()" withArguments:@{ } error:&error];
return error ?: obj;
}
id getNotificationPermission()
{
return [m_webView stringByEvaluatingJavaScript:@"Notification.permission"];
}
id getNotificationPermissionFromServiceWorker()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await getNotificationPermissionFromServiceWorker()" withArguments:@{ } error:&error];
return error ?: obj;
}
id getPushPermissionState()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await getPushPermissionState()" withArguments:@{ } error:&error];
return error ?: obj;
}
id getPushPermissionStateFromServiceWorker()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await getPushPermissionStateFromServiceWorker()" withArguments:@{ } error:&error];
return error ?: obj;
}
id queryPermission(NSString *name)
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await queryPermission(name)" withArguments:@{ @"name": name } error:&error];
return error ?: obj;
}
id queryPermissionFromServiceWorker(NSString *name)
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await queryPermissionFromServiceWorker(name)" withArguments:@{ @"name": name } error:&error];
return error ?: obj;
}
id getPushSubscription()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await getPushSubscription()" withArguments:@{ } error:&error];
return error ?: obj;
}
bool hasPushSubscription()
{
return [getPushSubscription() isKindOfClass:[NSDictionary class]];
}
bool hasServiceWorkerRegistration()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await navigator.serviceWorker.getRegistration()" withArguments:@{ } error:&error];
return error ?: obj;
}
// Can be used in cases where the service worker was unregistered (in which case
// hasPushSubscription would fail, since PushManager.getSubscription() fails if there is no
// active service worker).
bool hasPushSubscriptionForTesting()
{
__block bool done = false;
__block bool result = false;
[m_dataStore _scopeURL:m_url.get() hasPushSubscriptionForTesting:^(BOOL fetchedResult) {
result = fetchedResult;
done = true;
}];
TestWebKitAPI::Util::run(&done);
return result;
}
id unregisterServiceWorker()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await globalRegistration.unregister()" withArguments:@{ } error:&error];
return error ?: obj;
}
void disableShowNotifications()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await disableShowNotifications()" withArguments:@{ } error:&error];
ASSERT_FALSE(error);
ASSERT_TRUE([obj isEqual:@YES]);
}
id getNotifications()
{
NSError *error = nil;
id obj = [m_webView objectByCallingAsyncFunction:@"return await getNotifications()" withArguments:@{ } error:&error];
return error ?: obj;
}
NSNumber *getAppBadge()
{
__block bool done = false;
__block NSNumber *result = nil;
[m_webView.get().configuration.websiteDataStore _getAppBadgeForTesting:^(NSNumber *badge) {
result = badge;
done = true;
}];
TestWebKitAPI::Util::run(&done);
return result;
}
void closeAllNotifications()
{
NSError *error = nil;
[m_webView objectByCallingAsyncFunction:@"return await closeAllNotifications()" withArguments:@{ } error:&error];
EXPECT_NULL(error);
}
void resetPermission()
{
m_notificationProvider.resetPermission(m_origin);
}
void setPermission(bool value)
{
m_notificationProvider.setPermission(m_origin, value);
}
#if ENABLE(DECLARATIVE_WEB_PUSH)
void injectDeclarativePushMessage(ASCIILiteral json)
{
WebKit::WebPushD::PushMessageForTesting message;
message.targetAppCodeSigningIdentifier = "com.apple.WebKit.TestWebKitAPI"_s;
message.registrationURL = URL("https://example.com"_s);
message.disposition = WebKit::WebPushD::PushMessageDisposition::Notification;
message.payload = json;
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
auto sender = WebPushXPCConnectionMessageSender { utilityConnection.get() };
__block bool done = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::InjectPushMessageForTesting(message), ^(const String& error) {
if (!error.isEmpty())
NSLog(@"ERROR: %s", error.utf8().data());
done = true;
});
TestWebKitAPI::Util::run(&done);
}
#endif
void clearMostRecents()
{
[m_delegate clearMostRecents];
}
_WKNotificationData *mostRecentNotification()
{
return m_delegate.get().mostRecentNotification.unsafeGet();
}
NSURL *mostRecentActionURL()
{
return m_delegate.get().mostRecentActionURL.unsafeGet();
}
std::optional<uint64_t> mostRecentAppBadge()
{
return m_delegate.get().mostRecentAppBadge;
}
void injectPushMessage(NSDictionary *apsUserInfo)
{
String scope = [m_url absoluteString];
WebCore::PushSubscriptionSetIdentifier subscriptionSetIdentifier {
.bundleIdentifier = "com.apple.WebKit.TestWebKitAPI"_s,
.pushPartition = m_pushPartition,
.dataStoreIdentifier = m_dataStoreIdentifier
};
auto topic = WebCore::makePushTopic(subscriptionSetIdentifier, scope);
id obj = @{
@"topic": topic.createNSString().get(),
@"userInfo": apsUserInfo
};
String message { byteCast<Latin1Character>(span([NSJSONSerialization dataWithJSONObject:obj options:0 error:nullptr])) };
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
auto sender = WebPushXPCConnectionMessageSender { utilityConnection.get() };
__block bool done = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::InjectEncryptedPushMessageForTesting(message), ^(bool injected) {
done = true;
});
TestWebKitAPI::Util::run(&done);
}
RetainPtr<NSDictionary> fetchPushMessage()
{
__block bool gotMessage = false;
__block RetainPtr<NSDictionary> message;
[m_dataStore _getPendingPushMessage:^(NSDictionary *rawMessage) {
message = rawMessage;
gotMessage = true;
}];
TestWebKitAPI::Util::run(&gotMessage);
return message;
}
RetainPtr<NSArray<NSDictionary *>> fetchPushMessages()
{
__block bool gotMessages = false;
__block RetainPtr<NSArray<NSDictionary *>> messages;
[m_dataStore _getPendingPushMessages:^(NSArray<NSDictionary *> *rawMessages) {
messages = rawMessages;
gotMessages = true;
}];
TestWebKitAPI::Util::run(&gotMessages);
return messages;
}
bool expectDecryptedMessage(NSString *expectedMessage, NSDictionary *message)
{
__block bool gotExpectedMessage = false;
[m_testMessageHandler addMessage:[NSString stringWithFormat:@"Received: %@", expectedMessage] withHandler:^{
gotExpectedMessage = true;
}];
// This will result in this process grabbing the queued pushes from webpushd and firing the push event in the service worker with that data.
__block bool pushMessageProcessed = false;
__block bool pushMessageProcessedResult = false;
[m_dataStore _processPushMessage:message completionHandler:^(bool result) {
pushMessageProcessedResult = result;
pushMessageProcessed = true;
}];
TestWebKitAPI::Util::run(&pushMessageProcessed);
TestWebKitAPI::Util::run(&gotExpectedMessage);
return pushMessageProcessedResult;
}
void expectDataStoreIdentifierSetOnLastNotification()
{
auto identifier = m_notificationProvider.lastNotificationDataStoreIdentifier();
if (m_dataStoreIdentifier)
EXPECT_WK_STREQ(m_dataStoreIdentifier->toString().utf8().data(), identifier);
else
EXPECT_NULL(identifier);
}
void simulateNotificationClick()
{
__block bool gotNotificationClick = false;
[m_testMessageHandler addMessage:@"Received: notificationclick" withHandler:^{
gotNotificationClick = true;
}];
ASSERT_TRUE(m_notificationProvider.simulateNotificationClick());
TestWebKitAPI::Util::run(&gotNotificationClick);
}
void setITPTimeAdvance(unsigned daysToAdvance)
{
static constexpr Seconds days { 3600.0 * 24 };
auto advance = days * daysToAdvance;
__block bool done = false;
[m_dataStore _setResourceLoadStatisticsTimeAdvanceForTesting:advance.value() completionHandler:^{
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
[m_dataStore _processStatisticsAndDataRecords:^{
done = true;
}];
Util::run(&done);
}
void assertPushEventSucceeds(unsigned daysToAdvance)
{
setITPTimeAdvance(daysToAdvance);
injectPushMessage(@{ });
auto messages = fetchPushMessages();
ASSERT_EQ([messages count], 1u) << "Unexpected push event injection failure after advancing timer by " << daysToAdvance << " days; spurious ITP cleanup?";
expectDecryptedMessage(@"null data", [messages firstObject]);
}
void assertPushEventFails(unsigned daysToAdvance)
{
setITPTimeAdvance(daysToAdvance);
injectPushMessage(@{ });
auto messages = fetchPushMessages();
ASSERT_EQ([messages count], 0u) << "Unexpected push event injection success after advancing ITP timer by " << daysToAdvance << " days; missing ITP cleanup?";
}
void processPushMessage(NSDictionary *pushMessage)
{
__block bool done = false;
[m_dataStore _processPushMessage:pushMessage completionHandler:^(bool result) {
done = true;
}];
TestWebKitAPI::Util::run(&done);
}
void captureAllMessages()
{
[m_testMessageHandler setDidReceiveScriptMessage:^(NSString *message) {
m_mostRecentMessage = message;
}];
}
const String& mostRecentMessage() const
{
return m_mostRecentMessage;
}
NSURL *url() const { return m_url.get(); }
private:
String m_pushPartition;
Markable<WTF::UUID> m_dataStoreIdentifier;
String m_origin;
RetainPtr<NSURL> m_url;
RetainPtr<WKWebsiteDataStore> m_dataStore;
RetainPtr<PushNotificationDelegate> m_delegate;
RetainPtr<TestMessageHandler> m_testMessageHandler;
std::unique_ptr<TestWebKitAPI::HTTPServer> m_server;
TestNotificationProvider& m_notificationProvider;
RetainPtr<WKWebView> m_webView;
String m_mostRecentMessage;
};
class WebPushDTest : public ::testing::Test {
public:
WebPushDTest(LaunchOnlyOnce launchOnlyOnce = LaunchOnlyOnce::Yes, ASCIILiteral html = htmlSource, InstallDataStoreDelegate installDataStoreDelegate = InstallDataStoreDelegate::No, BuiltInNotificationsEnabled builtInNotificationsEnabled = BuiltInNotificationsEnabled::No)
: m_html(html)
, m_installDataStoreDelegate(installDataStoreDelegate)
, m_builtInNotificationsEnabled(builtInNotificationsEnabled)
{
m_tempDirectory = retainPtr(setUpTestWebPushD(launchOnlyOnce));
}
void SetUp() override
{
auto processPoolConfiguration = adoptNS([[_WKProcessPoolConfiguration alloc] init]);
auto processPool = adoptNS([[WKProcessPool alloc] _initWithConfiguration:processPoolConfiguration.get()]);
m_notificationProvider = makeUnique<TestWebKitAPI::TestNotificationProvider>(Vector<WKNotificationManagerRef> { [processPool _notificationManagerForTesting], WKNotificationManagerGetSharedServiceWorkerNotificationManager() });
auto webView = makeUniqueRef<WebPushDTestWebView>(emptyString(), std::nullopt, processPool.get(), *m_notificationProvider, m_html, m_installDataStoreDelegate, m_builtInNotificationsEnabled);
m_webViews.append(WTF::move(webView));
auto webViewWithIdentifier1 = makeUniqueRef<WebPushDTestWebView>(emptyString(), WTF::UUID::parse("0bf5053b-164c-4b7d-8179-832e6bf158df"_s), processPool.get(), *m_notificationProvider, m_html, m_installDataStoreDelegate, m_builtInNotificationsEnabled);
m_webViews.append(WTF::move(webViewWithIdentifier1));
auto webViewWithIdentifier2 = makeUniqueRef<WebPushDTestWebView>(emptyString(), WTF::UUID::parse("940e7729-738e-439f-a366-1a8719e23b2d"_s), processPool.get(), *m_notificationProvider, m_html, m_installDataStoreDelegate, m_builtInNotificationsEnabled);
m_webViews.append(WTF::move(webViewWithIdentifier2));
auto webViewWithPartition = makeUniqueRef<WebPushDTestWebView>("testPartition"_s, std::nullopt, processPool.get(), *m_notificationProvider, m_html, m_installDataStoreDelegate, m_builtInNotificationsEnabled);
m_webViews.append(WTF::move(webViewWithPartition));
auto webViewWithPartitionAndIdentifier = makeUniqueRef<WebPushDTestWebView>("testPartition"_s, WTF::UUID::parse("940e7729-738e-439f-a366-1a8719e23b2d"_s), processPool.get(), *m_notificationProvider, m_html, m_installDataStoreDelegate, m_builtInNotificationsEnabled);
m_webViews.append(WTF::move(webViewWithPartitionAndIdentifier));
}
~WebPushDTest()
{
cleanUpTestWebPushD(m_tempDirectory.get());
}
Vector<UniqueRef<WebPushDTestWebView>>& webViews() { return m_webViews; }
std::pair<Vector<String>, Vector<String>> getPushTopics()
{
Vector<String> enabledTopics;
Vector<String> ignoredTopics;
auto connection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
auto sender = WebPushXPCConnectionMessageSender { connection.get() };
bool done = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::GetPushTopicsForTesting(), [&](Vector<String> enabled, Vector<String> ignored) {
enabledTopics = enabled;
ignoredTopics = ignored;
done = true;
});
TestWebKitAPI::Util::run(&done);
return std::make_pair(WTF::move(enabledTopics), WTF::move(ignoredTopics));
}
size_t subscribedTopicsCount() { return getPushTopics().first.size(); }
protected:
RetainPtr<NSURL> m_tempDirectory;
std::unique_ptr<TestWebKitAPI::TestNotificationProvider> m_notificationProvider;
Vector<UniqueRef<WebPushDTestWebView>> m_webViews;
ASCIILiteral m_html;
InstallDataStoreDelegate m_installDataStoreDelegate { InstallDataStoreDelegate::No };
BuiltInNotificationsEnabled m_builtInNotificationsEnabled { BuiltInNotificationsEnabled::No };
};
class WebPushDMultipleLaunchTest : public WebPushDTest {
public:
WebPushDMultipleLaunchTest()
: WebPushDTest(LaunchOnlyOnce::No)
{
}
};
class WebPushDNavigatorTest : public WebPushDTest {
public:
WebPushDNavigatorTest()
: WebPushDTest(LaunchOnlyOnce::Yes, navigatorHTMLSource)
{
}
};
TEST_F(WebPushDTest, SubscribeTest)
{
for (auto& v : webViews()) {
ASSERT_FALSE(v->hasPushSubscription());
id obj = v->subscribe();
ASSERT_TRUE(v->hasPushSubscription());
ASSERT_TRUE([obj isKindOfClass:[NSDictionary class]]);
NSDictionary *subscription = obj;
ASSERT_TRUE([subscription[@"endpoint"] hasPrefix:@"https://"]);
ASSERT_TRUE([subscription[@"keys"] isKindOfClass:[NSDictionary class]]);
// Shared auth secret should be 16 bytes (22 bytes in unpadded base64url).
ASSERT_EQ([subscription[@"keys"][@"auth"] length], 22u);
// Client public key should be 65 bytes (87 bytes in unpadded base64url).
ASSERT_EQ([subscription[@"keys"][@"p256dh"] length], 87u);
}
auto lessThan = [](const String& lhs, const String& rhs) {
return codePointCompare(lhs, rhs) < 0;
};
auto topics = getPushTopics();
auto& subscribed = topics.first;
std::ranges::sort(subscribed, lessThan);
Vector<String> expected {
"com.apple.WebKit.TestWebKitAPI ds:0bf5053b-164c-4b7d-8179-832e6bf158df https://example.com/"_s,
"com.apple.WebKit.TestWebKitAPI ds:940e7729-738e-439f-a366-1a8719e23b2d https://example.com/"_s,
"com.apple.WebKit.TestWebKitAPI https://example.com/"_s,
"com.apple.WebKit.TestWebKitAPI part:testPartition ds:940e7729-738e-439f-a366-1a8719e23b2d https://example.com/"_s,
"com.apple.WebKit.TestWebKitAPI part:testPartition https://example.com/"_s
};
ASSERT_EQ(subscribed, expected);
auto& ignored = topics.second;
ASSERT_EQ(ignored.size(), 0u);
}
TEST_F(WebPushDTest, SubscribeWithBadIPCVersionRaisesExceptionTest)
{
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
auto sender = WebPushXPCConnectionMessageSender { utilityConnection.get() };
bool done = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::SetProtocolVersionForTesting(WebKit::WebPushD::protocolVersionValue + 1), [&done]() {
done = true;
});
TestWebKitAPI::Util::run(&done);
for (auto& v : webViews()) {
ASSERT_FALSE(v->hasPushSubscription());
id obj = v->subscribe();
ASSERT_TRUE([obj isEqual:@"Error: AbortError: Connection to web push daemon failed"]);
}
}
#if ENABLE(DECLARATIVE_WEB_PUSH)
TEST_F(WebPushDNavigatorTest, SubscribeTest)
{
for (auto& v : webViews()) {
ASSERT_FALSE(v->hasPushSubscription());
id obj = v->subscribe();
ASSERT_TRUE(v->hasPushSubscription());
ASSERT_TRUE([obj isKindOfClass:[NSDictionary class]]);
NSDictionary *subscription = obj;
ASSERT_TRUE([subscription[@"endpoint"] hasPrefix:@"https://"]);
ASSERT_TRUE([subscription[@"keys"] isKindOfClass:[NSDictionary class]]);
// Shared auth secret should be 16 bytes (22 bytes in unpadded base64url).
ASSERT_EQ([subscription[@"keys"][@"auth"] length], 22u);
// Client public key should be 65 bytes (87 bytes in unpadded base64url).
ASSERT_EQ([subscription[@"keys"][@"p256dh"] length], 87u);
}
auto lessThan = [](const String& lhs, const String& rhs) {
return codePointCompare(lhs, rhs) < 0;
};
auto topics = getPushTopics();
auto& subscribed = topics.first;
std::ranges::sort(subscribed, lessThan);
Vector<String> expected {
"com.apple.WebKit.TestWebKitAPI ds:0bf5053b-164c-4b7d-8179-832e6bf158df https://example.com/"_s,
"com.apple.WebKit.TestWebKitAPI ds:940e7729-738e-439f-a366-1a8719e23b2d https://example.com/"_s,
"com.apple.WebKit.TestWebKitAPI https://example.com/"_s,
"com.apple.WebKit.TestWebKitAPI part:testPartition ds:940e7729-738e-439f-a366-1a8719e23b2d https://example.com/"_s,
"com.apple.WebKit.TestWebKitAPI part:testPartition https://example.com/"_s
};
ASSERT_EQ(subscribed, expected);
auto& ignored = topics.second;
ASSERT_EQ(ignored.size(), 0u);
}
#endif // ENABLE(DECLARATIVE_WEB_PUSH)
TEST_F(WebPushDTest, SubscribeFailureTest)
{
for (auto& v : webViews()) {
ASSERT_FALSE(v->hasPushSubscription());
id obj = v->subscribe(keyThatCausesInjectedFailure);
ASSERT_FALSE(v->hasPushSubscription());
// Spec says that an error in the push service should be an AbortError.
ASSERT_TRUE([obj isKindOfClass:[NSString class]]);
ASSERT_TRUE([obj hasPrefix:@"Error: AbortError"]);
}
ASSERT_EQ(subscribedTopicsCount(), 0u);
}
#if ENABLE(DECLARATIVE_WEB_PUSH)
TEST_F(WebPushDNavigatorTest, SubscribeFailureTest)
{
for (auto& v : webViews()) {
ASSERT_FALSE(v->hasPushSubscription());
id obj = v->subscribe(keyThatCausesInjectedFailure);
ASSERT_FALSE(v->hasPushSubscription());
// Spec says that an error in the push service should be an AbortError.
ASSERT_TRUE([obj isKindOfClass:[NSString class]]);
ASSERT_TRUE([obj hasPrefix:@"Error: AbortError"]);
}
ASSERT_EQ(subscribedTopicsCount(), 0u);
}
#endif
TEST_F(WebPushDTest, UnsubscribeTest)
{
for (auto& v : webViews())
v->subscribe();
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
int i = 1;
for (auto& v : webViews()) {
ASSERT_TRUE(v->hasPushSubscription());
// First unsubscribe should succeed.
ASSERT_TRUE([v->unsubscribe() isEqual:@YES]);
ASSERT_FALSE(v->hasPushSubscription());
// Second unsubscribe should fail since the first one removed the record already.
ASSERT_TRUE([v->unsubscribe() isEqual:@NO]);
ASSERT_FALSE(v->hasPushSubscription());
// Unsubscribing from this data store should not affect subscriptions in other data stores.
ASSERT_EQ(subscribedTopicsCount(), webViews().size() - i);
i++;
}
}
#if ENABLE(DECLARATIVE_WEB_PUSH)
TEST_F(WebPushDNavigatorTest, UnsubscribeTest)
{
for (auto& v : webViews())
v->subscribe();
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
int i = 1;
for (auto& v : webViews()) {
ASSERT_TRUE(v->hasPushSubscription());
// First unsubscribe should succeed.
ASSERT_TRUE([v->unsubscribe() isEqual:@YES]);
ASSERT_FALSE(v->hasPushSubscription());
// Second unsubscribe should fail since the first one removed the record already.
ASSERT_TRUE([v->unsubscribe() isEqual:@NO]);
ASSERT_FALSE(v->hasPushSubscription());
// Unsubscribing from this data store should not affect subscriptions in other data stores.
ASSERT_EQ(subscribedTopicsCount(), webViews().size() - i);
i++;
}
}
#endif
TEST_F(WebPushDTest, UnsubscribesOnServiceWorkerUnregisterTest)
{
for (auto& v : webViews())
v->subscribe();
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
for (auto& v : webViews()) {
ASSERT_TRUE(v->hasPushSubscription());
id result = v->unregisterServiceWorker();
ASSERT_TRUE([result isEqual:@YES]);
ASSERT_TRUE(v->hasPushSubscription());
}
}
TEST_F(WebPushDTest, UnsubscribesOnClearingAllWebsiteData)
{
for (auto& v : webViews())
v->subscribe();
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
int i = 1;
for (auto& v : webViews()) {
ASSERT_TRUE(v->hasPushSubscription());
__block bool removedData = false;
[v->dataStore() removeDataOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] modifiedSince:[NSDate distantPast] completionHandler:^(void) {
removedData = true;
}];
TestWebKitAPI::Util::run(&removedData);
ASSERT_FALSE(v->hasPushSubscription());
// Unsubscribing from this data store should not affect subscriptions in other data stores.
ASSERT_EQ(subscribedTopicsCount(), webViews().size() - i);
i++;
}
}
TEST_F(WebPushDTest, UnsubscribesOnClearingWebsiteDataForOrigin)
{
for (auto& v : webViews())
v->subscribe();
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
int i = 1;
for (auto& v : webViews()) {
// First unsubscribe should succeed.
ASSERT_TRUE(v->hasPushSubscription());
__block bool fetchedRecords = false;
__block RetainPtr<NSArray<WKWebsiteDataRecord *>> records;
[v->dataStore() fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) {
records = dataRecords;
fetchedRecords = true;
}];
TestWebKitAPI::Util::run(&fetchedRecords);
RetainPtr vOrigin = v->origin().createNSString();
WKWebsiteDataRecord *filteredRecord = nil;
for (WKWebsiteDataRecord *record in records.get()) {
for (NSString *originString in record._originsStrings) {
if ([originString isEqualToString:vOrigin.get()]) {
filteredRecord = record;
break;
}
}
}
ASSERT_TRUE(filteredRecord);
__block bool removedData = false;
[v->dataStore() removeDataOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] forDataRecords:[NSArray arrayWithObject:filteredRecord] completionHandler:^(void) {
removedData = true;
}];
TestWebKitAPI::Util::run(&removedData);
ASSERT_FALSE(v->hasPushSubscription());
// Unsubscribing from this data store should not affect subscriptions in other data stores.
ASSERT_EQ(subscribedTopicsCount(), webViews().size() - i);
i++;
}
}
TEST_F(WebPushDTest, UnsubscribesOnPermissionReset)
{
// FIXME: test on all webviews once we finish refactoring the shared service worker notification
// managers to be datastore-aware.
auto& v = webViews().last();
v->subscribe();
ASSERT_TRUE(v->hasPushSubscription());
v->resetPermission();
bool isSubscribed = true;
TestWebKitAPI::Util::waitForConditionWithLogging([&v, &isSubscribed]() mutable {
isSubscribed = v->hasPushSubscription();
if (!isSubscribed)
return true;
sleep(1);
return false;
}, 5, @"Timed out waiting for push subscription to be removed.");
ASSERT_FALSE(isSubscribed);
}
TEST_F(WebPushDTest, IgnoresSubscriptionOnPermissionDenied)
{
// FIXME: test on all webviews once we finish refactoring the shared service worker notification
// managers to be datastore-aware.
auto& v = webViews().last();
v->subscribe();
ASSERT_TRUE(v->hasPushSubscription());
// Topic should be moved to ignored list after denying permission, but the subscription should still exist.
v->setPermission(false);
bool isIgnored = false;
TestWebKitAPI::Util::waitForConditionWithLogging([this, &isIgnored] {
auto [enabledTopics, ignoredTopics] = getPushTopics();
if (!enabledTopics.size() && ignoredTopics.size()) {
isIgnored = true;
return true;
}
sleep(1);
return false;
}, 5, @"Timed out waiting for push subscription to be ignored.");
ASSERT_TRUE(isIgnored);
ASSERT_TRUE(v->hasPushSubscription());
// Topic should be moved back to enabled list after allowing permission, and the subscription should still exist.
v->setPermission(true);
bool isEnabled = false;
TestWebKitAPI::Util::waitForConditionWithLogging([this, &isEnabled] {
auto [enabledTopics, ignoredTopics] = getPushTopics();
if (enabledTopics.size() && !ignoredTopics.size()) {
isEnabled = true;
return true;
}
sleep(1);
return false;
}, 5, @"Timed out waiting for push subscription to be enabled.");
ASSERT_TRUE(isEnabled);
ASSERT_TRUE(v->hasPushSubscription());
}
TEST_F(WebPushDTest, TooManySilentPushesCausesUnsubscribe)
{
for (auto& v : webViews()) {
v->subscribe();
v->disableShowNotifications();
}
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
int i = 1;
for (auto& v : webViews()) {
ASSERT_TRUE(v->hasPushSubscription());
for (unsigned i = 0; i < WebKit::WebPushD::maxSilentPushCount; i++) {
v->injectPushMessage(@{ });
auto messages = v->fetchPushMessages();
ASSERT_EQ([messages count], 1u);
// WebContent should fail processing the push since no notification was shown due to the
// disableShowNotifications call above.
ASSERT_FALSE(v->expectDecryptedMessage(@"null data", [messages firstObject]));
}
ASSERT_FALSE(v->hasPushSubscription());
// Unsubscribing from this data store should not affect subscriptions in other data stores.
ASSERT_EQ(subscribedTopicsCount(), webViews().size() - i);
i++;
}
}
TEST_F(WebPushDTest, GetPushSubscriptionWithMismatchedPublicToken)
{
for (auto& v : webViews())
v->subscribe();
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
// If the public token changes, all subscriptions should be invalidated.
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
auto sender = WebPushXPCConnectionMessageSender { utilityConnection.get() };
bool done = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::SetPublicTokenForTesting("foobar"_s), [&]() {
done = true;
});
TestWebKitAPI::Util::run(&done);
for (auto& v : webViews())
ASSERT_FALSE(v->hasPushSubscription());
ASSERT_EQ(subscribedTopicsCount(), 0u);
}
TEST_F(WebPushDMultipleLaunchTest, GetPushSubscriptionAfterDaemonRelaunch)
{
for (auto& v : webViews())
v->subscribe();
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
ASSERT_TRUE(restartTestWebPushD());
// Make sure that getSubscription works after killing webpushd. Previously, this didn't work and
// would fail with an AbortError because we didn't re-send the connection configuration after
// the daemon relaunched.
//
// Note that getSubscription() will return null now since we launch webpushd in in-memory mode
// when running tests. We're just making sure that this doesn't fail with an AbortError.
for (auto& v : webViews()) {
id result = v->getPushSubscription();
ASSERT_TRUE([result isEqual:[NSNull null]]);
}
ASSERT_EQ(subscribedTopicsCount(), 0u);
}
class WebPushDBuiltInTest : public WebPushDTest {
public:
WebPushDBuiltInTest()
: WebPushDTest(LaunchOnlyOnce::Yes, htmlSource, InstallDataStoreDelegate::Yes, BuiltInNotificationsEnabled::Yes)
{
}
};
#if HAVE(FULL_FEATURED_USER_NOTIFICATIONS)
TEST_F(WebPushDBuiltInTest, ShowAndGetNotifications)
{
auto& view = webViews().last();
view->subscribe();
auto dataStore = view->dataStore();
auto configuration = defaultWebPushDaemonConfiguration();
configuration.pushPartitionString = dataStore.get()._webPushPartition;
configuration.dataStoreIdentifier = WTF::UUID::fromNSUUID(dataStore.get()._identifier);
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service", WTF::move(configuration));
auto sender = WebPushXPCConnectionMessageSender { utilityConnection.get() };
WebKit::WebPushD::PushMessageForTesting message;
message.targetAppCodeSigningIdentifier = "com.apple.WebKit.TestWebKitAPI"_s;
message.pushPartitionString = dataStore.get()._webPushPartition;
message.registrationURL = view->url();
message.disposition = WebKit::WebPushD::PushMessageDisposition::Legacy;
message.payload = @"hello";
// No badge had been set, so confirm its `nil`
EXPECT_FALSE(view->getAppBadge());
done = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::InjectPushMessageForTesting(message), ^(const String& error) {
if (!error.isEmpty())
NSLog(@"ERROR: %s", error.utf8().data());
done = true;
});
TestWebKitAPI::Util::run(&done);
done = false;
RetainPtr delegate = (PushNotificationDelegate *)dataStore.get()._delegate;
[dataStore _getPendingPushMessages:^(NSArray<NSDictionary *> *messages) {
EXPECT_EQ(messages.count, 1u);
[dataStore _processPushMessage:messages.firstObject completionHandler:^(bool handled) {
EXPECT_TRUE(handled);
done = true;
}];
}];
TestWebKitAPI::Util::run(&done);
id result = view->getNotifications();
EXPECT_TRUE([result isEqualToString:@"0 - title: notification body: tag: dir: auto silent: null data: null "]);
view->closeAllNotifications();
result = view->getNotifications();
EXPECT_TRUE([result isEqualToString:@""]);
// The push message handler should set the app badge to 42
EXPECT_TRUE([view->getAppBadge() isEqual:@42]);
}
TEST_F(WebPushDBuiltInTest, TestPermissionsAfterNotificatonRequestPermission)
{
auto& view = webViews().last();
EXPECT_TRUE([view->getNotificationPermission() isEqual:@"default"]);
EXPECT_TRUE([view->getNotificationPermissionFromServiceWorker() isEqualToString:@"default"]);
EXPECT_TRUE([view->getPushPermissionState() isEqual:@"prompt"]);
EXPECT_TRUE([view->getPushPermissionStateFromServiceWorker() isEqualToString:@"prompt"]);
EXPECT_TRUE([view->queryPermission(@"push") isEqual:@"prompt"]);
EXPECT_TRUE([view->queryPermissionFromServiceWorker(@"push") isEqual:@"prompt"]);
EXPECT_TRUE([view->queryPermission(@"notifications") isEqual:@"prompt"]);
EXPECT_TRUE([view->queryPermissionFromServiceWorker(@"notifications") isEqual:@"prompt"]);
EXPECT_TRUE([view->requestNotificationPermission() isEqual:@"granted"]);
EXPECT_TRUE([view->getNotificationPermission() isEqual:@"granted"]);
EXPECT_TRUE([view->getNotificationPermissionFromServiceWorker() isEqualToString:@"granted"]);
EXPECT_TRUE([view->getPushPermissionState() isEqual:@"granted"]);
EXPECT_TRUE([view->getPushPermissionStateFromServiceWorker() isEqualToString:@"granted"]);
EXPECT_TRUE([view->queryPermission(@"push") isEqual:@"granted"]);
EXPECT_TRUE([view->queryPermissionFromServiceWorker(@"push") isEqual:@"granted"]);
EXPECT_TRUE([view->queryPermission(@"notifications") isEqual:@"granted"]);
EXPECT_TRUE([view->queryPermissionFromServiceWorker(@"notifications") isEqual:@"granted"]);
}
TEST_F(WebPushDBuiltInTest, TestPermissionsAfterSubscribe)
{
auto& view = webViews().last();
EXPECT_FALSE(view->hasPushSubscription());
EXPECT_TRUE([view->getNotificationPermission() isEqual:@"default"]);
EXPECT_TRUE([view->getNotificationPermissionFromServiceWorker() isEqualToString:@"default"]);
EXPECT_TRUE([view->getPushPermissionState() isEqual:@"prompt"]);
EXPECT_TRUE([view->getPushPermissionStateFromServiceWorker() isEqualToString:@"prompt"]);
EXPECT_TRUE([view->queryPermission(@"push") isEqual:@"prompt"]);
EXPECT_TRUE([view->queryPermissionFromServiceWorker(@"push") isEqual:@"prompt"]);
EXPECT_TRUE([view->queryPermission(@"notifications") isEqual:@"prompt"]);
EXPECT_TRUE([view->queryPermissionFromServiceWorker(@"notifications") isEqual:@"prompt"]);
view->subscribe();
EXPECT_TRUE(view->hasPushSubscription());
EXPECT_TRUE([view->getNotificationPermission() isEqual:@"granted"]);
EXPECT_TRUE([view->getNotificationPermissionFromServiceWorker() isEqualToString:@"granted"]);
EXPECT_TRUE([view->getPushPermissionState() isEqual:@"granted"]);
EXPECT_TRUE([view->getPushPermissionStateFromServiceWorker() isEqualToString:@"granted"]);
EXPECT_TRUE([view->queryPermission(@"push") isEqual:@"granted"]);
EXPECT_TRUE([view->queryPermissionFromServiceWorker(@"push") isEqual:@"granted"]);
EXPECT_TRUE([view->queryPermission(@"notifications") isEqual:@"granted"]);
EXPECT_TRUE([view->queryPermissionFromServiceWorker(@"notifications") isEqual:@"granted"]);
}
TEST_F(WebPushDBuiltInTest, ImplicitSilentPushTimerCancelledOnShowingNotification)
{
for (auto& v : webViews())
v->subscribe();
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
for (auto& v : webViews()) {
ASSERT_TRUE(v->hasPushSubscription());
for (unsigned i = 0; i < WebKit::WebPushD::maxSilentPushCount; i++) {
v->injectPushMessage(@{ });
auto message = v->fetchPushMessage();
ASSERT_TRUE(v->expectDecryptedMessage(@"null data", message.get()));
}
[NSThread sleepForTimeInterval:(WebKit::WebPushD::silentPushTimeoutForTesting.seconds() + 0.5)];
ASSERT_TRUE(v->hasPushSubscription());
}
}
TEST_F(WebPushDBuiltInTest, ImplicitSilentPushTimerIgnoredForInspectedContexts)
{
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
auto sender = WebPushXPCConnectionMessageSender { utilityConnection.get() };
auto setServiceWorkerIsBeingInspected = [&](const String& originString) {
__block bool done = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::SetServiceWorkerIsBeingInspected(URL(originString), true), ^() {
done = true;
});
TestWebKitAPI::Util::run(&done);
};
for (auto& v : webViews()) {
v->subscribe();
v->disableShowNotifications();
}
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
for (auto& v : webViews()) {
ASSERT_TRUE(v->hasPushSubscription());
setServiceWorkerIsBeingInspected(v->origin());
for (unsigned i = 0; i < WebKit::WebPushD::maxSilentPushCount; i++) {
v->injectPushMessage(@{ });
auto message = v->fetchPushMessage();
// _processPushMessage should return false since we disabled showing notifications above.
ASSERT_FALSE(v->expectDecryptedMessage(@"null data", message.get()));
}
// Should still be subscribed since we don't enforce the silent push timer while being inspected.
[NSThread sleepForTimeInterval:(WebKit::WebPushD::silentPushTimeoutForTesting.seconds() + 0.5)];
ASSERT_TRUE(v->hasPushSubscription());
}
}
TEST_F(WebPushDBuiltInTest, ImplicitSilentPushTimerCausesUnsubscribe)
{
for (auto& v : webViews()) {
v->subscribe();
v->disableShowNotifications();
}
ASSERT_EQ(subscribedTopicsCount(), webViews().size());
int i = 1;
for (auto& v : webViews()) {
ASSERT_TRUE(v->hasPushSubscription());
for (unsigned i = 0; i < WebKit::WebPushD::maxSilentPushCount; i++) {
v->injectPushMessage(@{ });
auto message = v->fetchPushMessage();
// _processPushMessage should return false since we disabled showing notifications above.
ASSERT_FALSE(v->expectDecryptedMessage(@"null data", message.get()));
}
bool unsubscribed = false;
TestWebKitAPI::Util::waitForConditionWithLogging([&] {
unsubscribed = !v->hasPushSubscription();
[NSThread sleepForTimeInterval:0.25];
return unsubscribed;
}, 5, @"Timed out waiting for push subscription to be unsubscribed.");
ASSERT_TRUE(unsubscribed);
// Unsubscribing from this data store should not affect subscriptions in other data stores.
ASSERT_EQ(subscribedTopicsCount(), webViews().size() - i);
i++;
}
}
#endif // HAVE(FULL_FEATURED_USER_NOTIFICATIONS)
class WebPushDInjectedPushTest : public WebPushDTest {
public:
void runTest(NSString *expectedMessage, NSDictionary *apsUserInfo)
{
for (auto& v : webViews()) {
v->subscribe();
v->injectPushMessage(apsUserInfo);
}
// Make sure each data store has exactly one push message to process.
Vector<RetainPtr<NSDictionary>> rawMessages;
for (auto& v : webViews()) {
auto messages = v->fetchPushMessages();
ASSERT_EQ([messages count], 1u);
rawMessages.append([messages firstObject]);
}
int i = 0;
for (auto& v : webViews()) {
v->expectDecryptedMessage(expectedMessage, rawMessages[i++].get());
v->expectDataStoreIdentifierSetOnLastNotification();
}
}
};
TEST_F(WebPushDInjectedPushTest, HandleInjectedEmptyPush)
{
runTest(@"null data", @{ });
}
TEST_F(WebPushDInjectedPushTest, HandleInjectedAESGCMPush)
{
runTest(@"test aesgcm payload", @{
@"content_encoding": @"aesgcm",
@"as_publickey": @"BC-AgYMhqmzamH7_Aum0YvId8FV1-umgHweJNe6XQ1IMAm3E29loWXqTRndibxH27kJKWcIbyymundODMfVx_UM",
@"as_salt": @"tkPT5xDeN0lAkSc6lZUkNg",
@"payload": @"o/u4yvcXI1nap+zyIOBbWXdLqj1qHG2cX+KVhAdBQj1GVAt7lQ=="
});
}
TEST_F(WebPushDInjectedPushTest, HandleInjectedAES128GCMPush)
{
// From example in RFC8291 Section 5.
String payloadBase64URL = "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN"_s;
String payloadBase64 = base64EncodeToString(base64URLDecode(payloadBase64URL).value());
runTest(@"When I grow up, I want to be a watermelon", @{
@"content_encoding": @"aes128gcm",
@"payload": payloadBase64.createNSString().get()
});
}
TEST_F(WebPushDTest, NotificationClickExtendsITPCleanupTimerBy30Days)
{
// FIXME: test on all webviews once we finish refactoring the shared service worker notification
// managers to be datastore-aware.
auto& v = webViews().last();
v->subscribe();
EXPECT_TRUE(v->hasServiceWorkerRegistration());
EXPECT_TRUE(v->hasPushSubscription());
v->assertPushEventSucceeds(0);
v->assertPushEventSucceeds(29);
v->simulateNotificationClick();
EXPECT_TRUE(v->hasServiceWorkerRegistration());
EXPECT_TRUE(v->hasPushSubscription());
v->assertPushEventSucceeds(58);
EXPECT_TRUE(v->hasServiceWorkerRegistration());
EXPECT_TRUE(v->hasPushSubscription());
v->setITPTimeAdvance(61);
EXPECT_FALSE(v->hasServiceWorkerRegistration());
EXPECT_TRUE(v->hasPushSubscription());
// Verify that even though the service worker is gone, push messages do make it through (because the subscription is still active)
v->injectPushMessage(@{ });
auto messages = v->fetchPushMessages();
ASSERT_EQ([messages count], 1u) << "Unexpected push event injection failure after advancing ITP timer by 61 days; ITP cleanup removed subscription?";
}
#if ENABLE(DECLARATIVE_WEB_PUSH)
static constexpr ASCIILiteral json5 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "foo"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json6 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json7 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": ""
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json8 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": 4
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json9 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "",
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json10 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": -1,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json11 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": { },
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json12 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": 10,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json13 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"options": 0
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json14 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"options": { }
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json15 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"dir": 0
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json16 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"dir": "auto"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json17 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"dir": "ltr"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json18 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"dir": "rtl"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json19 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"dir": "nonsense"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json20 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"lang": { }
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json21 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"lang": "language"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json22 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"body": { }
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json23 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"body": "world"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json24 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"tag": { }
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json25 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"tag": "world"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json26 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"icon": 0
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json27 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"icon": "world"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json28 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"icon": "https://example.com/icon.png"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json29 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"silent": 0
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json30 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"silent": true
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json31 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"silent": false
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json32 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "20",
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json33 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "8031",
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json34 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "18446744073709551616",
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json35 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
},
"mutable": 39
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json36 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
},
"mutable": { }
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json37 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
},
"mutable": "true"
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json38 = R"JSONRESOURCE(
{
"web_push": 8030,
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
},
"mutable": true
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json39 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "12",
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!"
},
"mutable": true
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json40 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "12",
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"tag": "title Gotcha!"
},
"mutable": true
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json41 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "12",
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"tag": "badge 1024"
},
"mutable": true
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json42 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "12",
"notification": {
"navigate": "https://example.com/",
"title": "Hello world!",
"tag": "titleandbadge ThisRules 4096"
},
"mutable": true
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json43 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "12",
"notification": {
"navigate": "https://example.com/",
"title": "Test the data object",
"tag": "datatotitle",
"data": "Raw string"
},
"mutable": true
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json44 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "12",
"notification": {
"navigate": "https://example.com/",
"title": "Test the data object",
"tag": "datatotitle",
"data": { "key": "value" }
},
"mutable": true
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral json45 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "12",
"notification": {
"navigate": "https://example.com/",
"title": "Test a default action URL override",
"tag": "defaultactionurl https://webkit.org/"
},
"mutable": true
}
)JSONRESOURCE"_s;
// Intentionally keep mutable as a child of notification here until we fix webkit.org/b/297389.
static constexpr ASCIILiteral json46 = R"JSONRESOURCE(
{
"web_push": 8030,
"app_badge": "12",
"notification": {
"navigate": "https://example.com/",
"title": "Test a missing default action URL override",
"mutable": true,
"tag": "emptydefaultactionurl"
}
}
)JSONRESOURCE"_s;
static constexpr ASCIILiteral errors[] = {
"does not contain valid JSON"_s,
"top level JSON value is not an object"_s,
"'navigate' member is specified but does not represent a valid URL"_s,
"'title' member is missing or is an empty string"_s,
"'title' member is specified but is not a string"_s,
"'app_badge' member is specified as a string that did not parse to to an unsigned long long"_s,
"'app_badge' member is specified as an number but is not a valid unsigned long long"_s,
"<intentionally left blank>"_s,
"'app_badge' member is specified but is not a string or a number"_s,
"'dir' member is specified but is not a valid NotificationDirection"_s,
"'dir' member is specified but is not a string"_s,
"'lang' member is specified but is not a string"_s,
"'body' member is specified but is not a string"_s,
"'tag' member is specified but is not a string"_s,
"'icon' member is specified but is not a string"_s,
"'icon' member is specified but does not represent a valid URL"_s,
"'silent' member is specified but is not a boolean"_s,
"'app_badge' member is specified as a string that did not parse to a valid unsigned long long"_s,
"'mutable' member is specified but is not a boolean"_s
};
static std::pair<ASCIILiteral, ASCIILiteral> jsonAndErrors[] = {
{ json5, errors[2] },
{ json6, errors[3] },
{ json7, errors[3] },
{ json8, errors[4] },
{ json9, { " "_s } },
{ json10, errors[6] },
{ json11, errors[8] },
{ json12, { " "_s } },
{ json13, { " "_s } },
{ json14, { " "_s } },
{ json15, errors[10] },
{ json16, { " "_s } },
{ json17, { " "_s } },
{ json18, { " "_s } },
{ json19, errors[9] },
{ json20, errors[11] },
{ json21, { " "_s } },
{ json22, errors[12] },
{ json23, { " "_s } },
{ json24, errors[13] },
{ json25, { " "_s } },
{ json26, errors[14] },
{ json27, errors[15] },
{ json28, { " "_s } },
{ json29, errors[16] },
{ json30, { " "_s } },
{ json31, { " "_s } },
{ json32, { " "_s } },
{ json33, { " "_s } },
{ json34, errors[17] },
{ json35, errors[18] },
{ json36, errors[18] },
{ json37, errors[18] },
{ json38, { " "_s } },
{ json39, { " "_s } },
{ json40, { " "_s } },
{ json41, { " "_s } },
{ json42, { " "_s } },
{ json43, { " "_s } },
{ json44, { " "_s } },
{ json45, { " "_s } },
{ json46, { " "_s } },
{ { }, { } }
};
static size_t expectedSuccessfulMessages()
{
size_t result = 0;
for (size_t i = 0; !jsonAndErrors[i].first.isNull(); ++i) {
if (!strcmp(jsonAndErrors[i].second, " "))
++result;
}
return result;
}
// Directly message the daemon to do JSON parsing validation on the declarative message
TEST(WebPushD, DeclarativeParsing)
{
setUpTestWebPushD();
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
auto dataStoreConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]);
dataStoreConfiguration.get().webPushMachServiceName = @"org.webkit.webpushtestdaemon.service";
auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]);
clearWebsiteDataStore(dataStore.get());
auto sender = WebPushXPCConnectionMessageSender { utilityConnection.get() };
static bool done = false;
WebKit::WebPushD::PushMessageForTesting message;
message.targetAppCodeSigningIdentifier = "com.apple.WebKit.TestWebKitAPI"_s;
message.registrationURL = URL("https://example.com"_s);
message.disposition = WebKit::WebPushD::PushMessageDisposition::Notification;
unsigned i = 0;
while (!jsonAndErrors[i].first.isNull()) {
message.payload = jsonAndErrors[i].first;
done = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::InjectPushMessageForTesting(message), [&](const String& error) {
if (!error.isEmpty())
EXPECT_TRUE(error.endsWith(jsonAndErrors[i].second));
else
EXPECT_FALSE(strcmp(jsonAndErrors[i].second, " "));
done = true;
});
TestWebKitAPI::Util::run(&done);
++i;
}
// Now retrieve the successfully parsed messages like a client would,
// but validate they make sense like you only can in internals.
done = false;
[dataStore _getPendingPushMessages:^(NSArray<NSDictionary *> *messages) {
EXPECT_EQ(messages.count, expectedSuccessfulMessages());
for (NSDictionary *message in messages)
EXPECT_TRUE(message[@"WebKitNotificationPayload"]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
}
// Verifies that handling a declarative web push message - with no service worker even registered - calls
// back into the client for showing the notification, etc.
TEST(WebPushD, DeclarativeWebPushHandling)
{
setUpTestWebPushD();
auto dataStoreConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]);
dataStoreConfiguration.get().webPushMachServiceName = @"org.webkit.webpushtestdaemon.service";
dataStoreConfiguration.get().isDeclarativeWebPushEnabled = YES;
auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]);
clearWebsiteDataStore(dataStore.get());
auto delegate = adoptNS([[PushNotificationDelegate alloc] init]);
dataStore.get()._delegate = delegate.get();
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
auto sender = WebPushXPCConnectionMessageSender { utilityConnection.get() };
static bool done = false;
WebKit::WebPushD::PushMessageForTesting message;
message.pushPartitionString = "TestWebKitAPI"_s;
message.targetAppCodeSigningIdentifier = "com.apple.WebKit.TestWebKitAPI"_s;
message.registrationURL = URL("https://example.com"_s);
message.disposition = WebKit::WebPushD::PushMessageDisposition::Notification;
message.payload = json33;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::InjectPushMessageForTesting(message), [&](const String& error) {
EXPECT_TRUE(error.isEmpty());
done = true;
});
TestWebKitAPI::Util::run(&done);
// Verify that even after having sent a push message to the daemon, there are no pending messages, as it was already handled.
done = false;
[dataStore _getPendingPushMessages:^(NSArray<NSDictionary *> *messages) {
EXPECT_EQ(messages.count, 0u);
done = true;
}];
TestWebKitAPI::Util::run(&done);
#if HAVE(FULL_FEATURED_USER_NOTIFICATIONS)
RetainPtr configuration = adoptNS([[_WKWebPushDaemonConnectionConfiguration alloc] init]);
configuration.get().machServiceName = @"org.webkit.webpushtestdaemon.service";
configuration.get().bundleIdentifierOverrideForTesting = @"com.apple.WebKit.TestWebKitAPI";
configuration.get().hostApplicationAuditToken = getSelfAuditToken();
configuration.get().partition = @"TestWebKitAPI";
RetainPtr connection = adoptNS([[_WKWebPushDaemonConnection alloc] initWithConfiguration:configuration.get()]);
done = false;
[connection getNotifications:message.registrationURL.createNSURL().get() tag:@"" completionHandler:^(NSArray<_WKNotificationData *> *notifications, NSError *error) {
EXPECT_NULL(error);
EXPECT_NOT_NULL(notifications);
EXPECT_EQ(notifications.count, 1u);
EXPECT_TRUE([notifications[0].title isEqualToString:@"Hello world!"]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
#endif // HAVE(FULL_FEATURED_USER_NOTIFICATIONS)
// FIXME: Figure out how to activate the notification programtically to verify the appropriate delegate callbacks are made
}
#if HAVE(FULL_FEATURED_USER_NOTIFICATIONS)
TEST(WebPushD, WKWebPushDaemonConnectionRequestPushPermission)
{
setUpTestWebPushD();
auto configuration = adoptNS([[_WKWebPushDaemonConnectionConfiguration alloc] init]);
configuration.get().machServiceName = @"org.webkit.webpushtestdaemon.service";
configuration.get().hostApplicationAuditToken = getSelfAuditToken();
auto connection = adoptNS([[_WKWebPushDaemonConnection alloc] initWithConfiguration:configuration.get()]);
auto url = adoptNS([[NSURL alloc] initWithString:@"https://webkit.org"]);
__block bool done = false;
[connection getPushPermissionStateForOrigin:url.get() completionHandler:^(_WKWebPushPermissionState state) {
done = true;
EXPECT_EQ(state, _WKWebPushPermissionStatePrompt);
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection requestPushPermissionForOrigin:url.get() completionHandler:^(BOOL granted) {
done = true;
EXPECT_TRUE(granted);
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection getPushPermissionStateForOrigin:url.get() completionHandler:^(_WKWebPushPermissionState state) {
done = true;
EXPECT_EQ(state, _WKWebPushPermissionStateGranted);
}];
TestWebKitAPI::Util::run(&done);
}
#endif
TEST(WebPushD, WKWebPushDaemonConnectionPushNotifications)
{
setUpTestWebPushD();
auto configuration = adoptNS([[_WKWebPushDaemonConnectionConfiguration alloc] init]);
configuration.get().machServiceName = @"org.webkit.webpushtestdaemon.service";
// Bundle identifier is required for making push subscription.
configuration.get().bundleIdentifierOverrideForTesting = @"com.apple.WebKit.TestWebKitAPI";
configuration.get().hostApplicationAuditToken = getSelfAuditToken();
auto connection = adoptNS([[_WKWebPushDaemonConnection alloc] initWithConfiguration:configuration.get()]);
auto url = adoptNS([[NSURL alloc] initWithString:@"https://webkit.org/sw.js"]);
RetainPtr applicationServerKey = [NSData dataWithBytes:(const void *)validServerKey.characters() length:validServerKey.length()];
__block bool done = false;
[connection subscribeToPushServiceForScope:url.get() applicationServerKey:applicationServerKey.get() completionHandler:^(_WKWebPushSubscriptionData *subscription, NSError *error) {
EXPECT_NULL(error);
EXPECT_NOT_NULL(subscription);
EXPECT_WK_STREQ(@"https://webkit.org/push", subscription.endpoint.absoluteString);
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection getSubscriptionForScope:url.get() completionHandler:^(_WKWebPushSubscriptionData *subscription, NSError *error) {
EXPECT_NULL(error);
EXPECT_NOT_NULL(subscription);
EXPECT_WK_STREQ(@"https://webkit.org/push", subscription.endpoint.absoluteString);
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection unsubscribeFromPushServiceForScope:url.get() completionHandler:^(BOOL unsubscribed, NSError * error) {
EXPECT_NULL(error);
EXPECT_TRUE(unsubscribed);
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection getSubscriptionForScope:url.get() completionHandler:^(_WKWebPushSubscriptionData *subscription, NSError *error) {
EXPECT_NULL(error);
EXPECT_NULL(subscription);
done = true;
}];
TestWebKitAPI::Util::run(&done);
#if HAVE(FULL_FEATURED_USER_NOTIFICATIONS)
done = false;
[connection getNotifications:url.get() tag:@"" completionHandler:^(NSArray<_WKNotificationData *> *notifications, NSError *error) {
EXPECT_NULL(error);
EXPECT_NOT_NULL(notifications);
EXPECT_EQ(notifications.count, 0u);
done = true;
}];
TestWebKitAPI::Util::run(&done);
RetainPtr notification = adoptNS([[_WKMutableNotificationData alloc] init]);
notification.get().title = @"Hello World!";
notification.get().dir = _WKNotificationDirectionLTR;
notification.get().lang = @"en";
notification.get().body = @"Body1";
notification.get().tag = @"Tag1";
notification.get().alert = _WKNotificationAlertSilent;
notification.get().data = [NSData data];
notification.get().securityOrigin = url.get();
notification.get().serviceWorkerRegistrationURL = url.get();
RetainPtr uuid1 = [NSUUID UUID];
notification.get().uuid = uuid1.get();
done = false;
[connection showNotification:notification.get() completionHandler:^{
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection getNotifications:url.get() tag:@"" completionHandler:^(NSArray<_WKNotificationData *> *notifications, NSError *error) {
EXPECT_NULL(error);
EXPECT_EQ(notifications.count, 1u);
if (notifications.count) {
EXPECT_TRUE([notifications[0].title isEqualToString:@"Hello World!"]);
EXPECT_EQ(notifications[0].dir, _WKNotificationDirectionLTR);
EXPECT_TRUE([notifications[0].lang isEqualToString:@"en"]);
EXPECT_TRUE([notifications[0].body isEqualToString:@"Body1"]);
EXPECT_TRUE([notifications[0].tag isEqualToString:@"Tag1"]);
EXPECT_EQ(notifications[0].alert, _WKNotificationAlertSilent);
EXPECT_TRUE([notifications[0].data isEqual:[NSData data]]);
EXPECT_TRUE([notifications[0].securityOrigin isEqual:notification.get().securityOrigin]);
EXPECT_TRUE([notifications[0].serviceWorkerRegistrationURL isEqual:url.get()]);
EXPECT_TRUE([notifications[0].uuid isEqual:uuid1.get()]);
}
done = true;
}];
TestWebKitAPI::Util::run(&done);
[connection cancelNotification:url.get() uuid:uuid1.get()];
done = false;
[connection getNotifications:url.get() tag:@"" completionHandler:^(NSArray<_WKNotificationData *> *notifications, NSError *error) {
EXPECT_NULL(error);
EXPECT_NOT_NULL(notifications);
EXPECT_EQ(notifications.count, 0u);
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection showNotification:notification.get() completionHandler:^{
done = true;
}];
TestWebKitAPI::Util::run(&done);
notification.get().body = @"Body2";
notification.get().tag = @"Tag2";
RetainPtr uuid2 = [NSUUID UUID];
notification.get().uuid = uuid2.get();
done = false;
[connection showNotification:notification.get() completionHandler:^{
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection getNotifications:url.get() tag:@"" completionHandler:^(NSArray<_WKNotificationData *> *notifications, NSError *error) {
EXPECT_NULL(error);
EXPECT_EQ(notifications.count, 2u);
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection getNotifications:url.get() tag:@"Tag1" completionHandler:^(NSArray<_WKNotificationData *> *notifications, NSError *error) {
EXPECT_NULL(error);
EXPECT_EQ(notifications.count, 1u);
if (notifications.count)
EXPECT_TRUE([notifications[0].body isEqualToString:@"Body1"]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
done = false;
[connection getNotifications:url.get() tag:@"Tag2" completionHandler:^(NSArray<_WKNotificationData *> *notifications, NSError *error) {
EXPECT_NULL(error);
EXPECT_EQ(notifications.count, 1u);
if (notifications.count)
EXPECT_TRUE([notifications[0].body isEqualToString:@"Body2"]);
done = true;
}];
TestWebKitAPI::Util::run(&done);
[connection cancelNotification:url.get() uuid:uuid1.get()];
done = false;
[connection getNotifications:url.get() tag:@"" completionHandler:^(NSArray<_WKNotificationData *> *notifications, NSError *error) {
EXPECT_NULL(error);
EXPECT_EQ(notifications.count, 1u);
if (notifications.count) {
EXPECT_TRUE([notifications[0].body isEqualToString:@"Body2"]);
EXPECT_TRUE([notifications[0].uuid isEqual:uuid2.get()]);
}
done = true;
}];
TestWebKitAPI::Util::run(&done);
#endif // HAVE(FULL_FEATURED_USER_NOTIFICATIONS)
}
TEST(WebPushD, WKWebPushDaemonConnectionSubscribeWithBadIPCVersionRaisesException)
{
setUpTestWebPushD();
auto utilityConnection = createAndConfigureConnectionToService("org.webkit.webpushtestdaemon.service");
auto sender = WebPushXPCConnectionMessageSender { utilityConnection.get() };
bool done = false;
sender.sendWithAsyncReplyWithoutUsingIPCConnection(Messages::PushClientConnection::SetProtocolVersionForTesting(WebKit::WebPushD::protocolVersionValue + 1), [&done]() {
done = true;
});
TestWebKitAPI::Util::run(&done);
auto configuration = adoptNS([[_WKWebPushDaemonConnectionConfiguration alloc] init]);
configuration.get().machServiceName = @"org.webkit.webpushtestdaemon.service";
// Bundle identifier is required for making push subscription.
configuration.get().bundleIdentifierOverrideForTesting = @"com.apple.WebKit.TestWebKitAPI";
configuration.get().hostApplicationAuditToken = getSelfAuditToken();
auto connection = adoptNS([[_WKWebPushDaemonConnection alloc] initWithConfiguration:configuration.get()]);
auto url = adoptNS([[NSURL alloc] initWithString:@"https://webkit.org/sw.js"]);
RetainPtr applicationServerKey = [NSData dataWithBytes:(const void *)validServerKey.characters() length:validServerKey.length()];
done = false;
RetainPtr<NSError> error;
[connection subscribeToPushServiceForScope:url.get() applicationServerKey:applicationServerKey.get() completionHandler:[&done, &error] (_WKWebPushSubscriptionData *subscription, NSError *subscriptionError) {
error = subscriptionError;
done = true;
}];
TestWebKitAPI::Util::run(&done);
ASSERT_TRUE([[error description] containsString:@"Connection to web push daemon failed"]);
}
class WebPushDPushNotificationEventTest : public WebPushDTest {
public:
WebPushDPushNotificationEventTest()
: WebPushDTest(LaunchOnlyOnce::Yes, htmlSource, InstallDataStoreDelegate::Yes)
{
}
void prep()
{
webViews().first()->subscribe();
}
void runTest(ASCIILiteral jsonMessage)
{
webViews().first()->clearMostRecents();
webViews().first()->injectDeclarativePushMessage(jsonMessage);
auto message = webViews().first()->fetchPushMessage();
EXPECT_TRUE(message);
webViews().first()->captureAllMessages();
webViews().first()->processPushMessage(message.get());
}
void waitForMessageAndVerify(NSString *message)
{
while (webViews().first()->mostRecentMessage().isEmpty())
TestWebKitAPI::Util::runFor(0.05_s);
EXPECT_TRUE([webViews().first()->mostRecentMessage().createNSString() isEqualToString:message]);
}
void checkLastNotificationTitle(NSString *title)
{
NSString *recentTitle = webViews().first()->mostRecentNotification().userInfo[@"WebNotificationTitleKey"];
EXPECT_TRUE([recentTitle isEqualToString:title]);
if (![recentTitle isEqualToString:title])
NSLog(@"Most recent title: %@\nExpected title: %@", recentTitle, title);
}
void checkLastNotificationDefaultActionURL(NSString *actionURL)
{
NSString *notificationActionURL = webViews().first()->mostRecentNotification().userInfo[@"WebNotificationDefaultActionURLKey"];
EXPECT_TRUE([notificationActionURL isEqualToString:actionURL]);
}
void checkLastActionURL(NSString *url)
{
NSURL *recentActionURL = webViews().first()->mostRecentActionURL();
EXPECT_TRUE([url isEqualToString:recentActionURL.absoluteString]);
if (![url isEqualToString:recentActionURL.absoluteString])
NSLog(@"Lact action URL: %@\nExpected URL: %@", recentActionURL, url);
}
void checkLastAppBadge(std::optional<uint64_t> badge)
{
EXPECT_EQ(badge, webViews().first()->mostRecentAppBadge());
}
};
TEST_F(WebPushDPushNotificationEventTest, Basic)
{
prep();
runTest(json39);
checkLastNotificationTitle(@"Hello world!");
checkLastAppBadge(12);
EXPECT_TRUE(webViews().first()->hasPushSubscription());
runTest(json40);
checkLastNotificationTitle(@"Gotcha!");
checkLastAppBadge(12);
EXPECT_TRUE(webViews().first()->hasPushSubscription());
runTest(json41);
checkLastNotificationTitle(@"Hello world!");
checkLastAppBadge(1024);
EXPECT_TRUE(webViews().first()->hasPushSubscription());
runTest(json42);
checkLastNotificationTitle(@"ThisRules");
checkLastAppBadge(4096);
EXPECT_TRUE(webViews().first()->hasPushSubscription());
runTest(json43);
checkLastNotificationTitle(@"Raw string");
checkLastAppBadge(12);
EXPECT_TRUE(webViews().first()->hasPushSubscription());
runTest(json44);
checkLastNotificationTitle(@"[object Object]");
checkLastAppBadge(12);
EXPECT_TRUE(webViews().first()->hasPushSubscription());
runTest(json45);
checkLastNotificationTitle(@"Test a default action URL override");
checkLastNotificationDefaultActionURL(@"https://webkit.org/");
checkLastAppBadge(12);
runTest(json46);
checkLastNotificationTitle(@"Test a missing default action URL override");
checkLastNotificationDefaultActionURL(@"https://example.com/");
waitForMessageAndVerify(@"showNotification failed: TypeError: Call to showNotification() while handling a `push` event did not include NotificationOptions that specify a valid defaultAction url");
// After the slew of above messages that were handled by service workers, silent push tracking should *not* have
// kicked in, and therefore there should still be a push subscription.
[NSThread sleepForTimeInterval:(WebKit::WebPushD::silentPushTimeoutForTesting.seconds() + 0.5)];
EXPECT_TRUE(webViews().first()->hasPushSubscription());
}
#endif // ENABLE(DECLARATIVE_WEB_PUSH)
} // namespace TestWebKitAPI
#endif // ENABLE(NOTIFICATIONS) && ENABLE(NOTIFICATION_EVENT) && (PLATFORM(MAC)