blob: c42edc7d5aaf754ede6e2d4bea4e1b954ce07b81 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web/webui/mojo_facade.h"
#import <memory>
#import "base/functional/bind.h"
#import "base/memory/raw_ptr.h"
#import "base/run_loop.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "ios/web/public/test/fakes/fake_web_frame.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_test.h"
#import "ios/web/test/mojo_test.test-mojom.h"
#import "ios/web/web_state/web_state_impl.h"
#import "testing/gtest_mac.h"
using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;
namespace web {
namespace {
// Serializes the given `object` to JSON string.
std::string GetJson(id object) {
NSData* json_as_data = [NSJSONSerialization dataWithJSONObject:object
options:0
error:nil];
NSString* json_as_string =
[[NSString alloc] initWithData:json_as_data
encoding:NSUTF8StringEncoding];
return base::SysNSStringToUTF8(json_as_string);
}
// Deserializes the given `json` to an object.
id GetObject(const std::string& json) {
NSData* json_as_data =
[base::SysUTF8ToNSString(json) dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:json_as_data
options:0
error:nil];
}
class FakeWebStateWithInterfaceBinder : public FakeWebState {
public:
InterfaceBinder* GetInterfaceBinderForMainFrame() override {
return &interface_binder_;
}
private:
InterfaceBinder interface_binder_{this};
};
} // namespace
// A test fixture to test MojoFacade class.
class MojoFacadeTest : public WebTest {
protected:
MojoFacadeTest() {
facade_ = std::make_unique<MojoFacade>(&web_state_);
auto web_frames_manager = std::make_unique<web::FakeWebFramesManager>();
frames_manager_ = web_frames_manager.get();
web_state_.SetWebFramesManager(std::move(web_frames_manager));
auto main_frame = FakeWebFrame::Create("frameID", /*is_main_frame=*/true);
main_frame_ = main_frame.get();
frames_manager_->AddWebFrame(std::move(main_frame));
}
FakeWebFrame* main_frame() { return main_frame_; }
MojoFacade* facade() { return facade_.get(); }
void CreateMessagePipe(uint32_t* handle0, uint32_t* handle1) {
NSDictionary* create = @{
@"name" : @"Mojo.createMessagePipe",
@"args" : @{},
};
std::string response_as_string =
facade()->HandleMojoMessage(GetJson(create));
// Verify handles.
ASSERT_FALSE(response_as_string.empty());
NSDictionary* response_as_dict = GetObject(response_as_string);
ASSERT_TRUE([response_as_dict isKindOfClass:[NSDictionary class]]);
ASSERT_EQ(MOJO_RESULT_OK, [response_as_dict[@"result"] unsignedIntValue]);
*handle0 = [response_as_dict[@"handle0"] unsignedIntValue];
*handle1 = [response_as_dict[@"handle1"] unsignedIntValue];
}
void CloseHandle(uint32_t handle) {
NSDictionary* close = @{
@"name" : @"MojoHandle.close",
@"args" : @{
@"handle" : @(handle),
},
};
std::string result = facade()->HandleMojoMessage(GetJson(close));
EXPECT_TRUE(result.empty());
}
NSDictionary* ReadMessage(uint32_t handle) {
// Read the message from the pipe.
NSDictionary* read = @{
@"name" : @"MojoHandle.readMessage",
@"args" : @{
@"handle" : @(handle),
},
};
NSDictionary* message =
GetObject(facade()->HandleMojoMessage(GetJson(read)));
EXPECT_TRUE([message isKindOfClass:[NSDictionary class]]);
return message;
}
int WatchHandle(uint32_t handle, int callback_id) {
NSDictionary* watch = @{
@"name" : @"MojoHandle.watch",
@"args" : @{
@"handle" : @(handle),
@"signals" : @(MOJO_HANDLE_SIGNAL_READABLE),
@"callbackId" : @(callback_id),
},
};
const std::string watch_id_as_string =
facade()->HandleMojoMessage(GetJson(watch));
EXPECT_FALSE(watch_id_as_string.empty());
int watch_id = 0;
EXPECT_TRUE(base::StringToInt(watch_id_as_string, &watch_id));
return watch_id;
}
void CancelWatch(uint32_t handle, int watch_id) {
NSDictionary* cancel_watch = @{
@"name" : @"MojoWatcher.cancel",
@"args" : @{
@"watchId" : @(watch_id),
},
};
EXPECT_TRUE(facade_->HandleMojoMessage(GetJson(cancel_watch)).empty());
}
void WriteMessage(uint32_t handle, NSString* buffer) {
NSDictionary* write = @{
@"name" : @"MojoHandle.writeMessage",
@"args" : @{@"handle" : @(handle), @"handles" : @[], @"buffer" : buffer},
};
const std::string result_as_string =
facade()->HandleMojoMessage(GetJson(write));
EXPECT_FALSE(result_as_string.empty());
unsigned result = 0u;
EXPECT_TRUE(base::StringToUint(result_as_string, &result));
EXPECT_EQ(MOJO_RESULT_OK, result);
}
std::string WaitForLastJavaScriptCall() {
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
// Flush any pending tasks. Don't RunUntilIdle() because
// RunUntilIdle() is incompatible with mojo::SimpleWatcher's
// automatic arming behavior, which Mojo JS still depends upon.
base::RunLoop loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, loop.QuitClosure());
loop.Run();
return !main_frame()->GetLastJavaScriptCall().empty();
}));
const auto last_js_call = main_frame()->GetLastJavaScriptCall();
main_frame()->ClearJavaScriptCallHistory();
return base::UTF16ToUTF8(last_js_call);
}
private:
FakeWebStateWithInterfaceBinder web_state_;
raw_ptr<web::FakeWebFramesManager> frames_manager_;
raw_ptr<FakeWebFrame> main_frame_;
std::unique_ptr<MojoFacade> facade_;
};
// Tests binding an interface.
TEST_F(MojoFacadeTest, BindInterface) {
uint32_t handle0 = 0;
uint32_t handle1 = 0;
CreateMessagePipe(&handle0, &handle1);
// Pass handle0 as interface request.
NSDictionary* connect = @{
@"name" : @"Mojo.bindInterface",
@"args" : @{
@"interfaceName" : @".TestUIHandlerMojo",
@"requestHandle" : @(handle0),
},
};
std::string handle_as_string = facade()->HandleMojoMessage(GetJson(connect));
EXPECT_TRUE(handle_as_string.empty());
CloseHandle(handle1);
}
// Tests creating a message pipe.
TEST_F(MojoFacadeTest, CreateMessagePipe) {
uint32_t handle0, handle1;
CreateMessagePipe(&handle0, &handle1);
CloseHandle(handle0);
CloseHandle(handle1);
}
// Tests watching the pipe.
TEST_F(MojoFacadeTest, Watch) {
uint32_t handle0, handle1;
CreateMessagePipe(&handle0, &handle1);
// Start watching one end of the pipe.
const int kCallbackId = 99;
WatchHandle(handle0, kCallbackId);
// Write to the other end of the pipe.
WriteMessage(handle1, @"QUJDRA=="); // "ABCD" in base-64
const auto expected_script = base::StringPrintf(
"Mojo.internal.watchCallbacksHolder.callCallback(%d, %d)", kCallbackId,
MOJO_RESULT_OK);
EXPECT_EQ(expected_script, WaitForLastJavaScriptCall());
CloseHandle(handle0);
CloseHandle(handle1);
}
TEST_F(MojoFacadeTest, WatcherRearming) {
uint32_t handle0, handle1;
CreateMessagePipe(&handle0, &handle1);
// Start watching one end of the pipe.
const int kCallbackId = 99;
WatchHandle(handle0, kCallbackId);
const auto expected_script = base::StringPrintf(
"Mojo.internal.watchCallbacksHolder.callCallback(%d, %d)", kCallbackId,
MOJO_RESULT_OK);
// Write to the other end of the pipe.
WriteMessage(handle1, @"QUJDRA=="); // "ABCD" in base-64
EXPECT_EQ(expected_script, WaitForLastJavaScriptCall());
// Read the pipe until MOJO_RESULT_SHOULD_WAIT is returned
// (the usual watcher behavior).
EXPECT_EQ([ReadMessage(handle0)[@"result"] unsignedIntValue], MOJO_RESULT_OK);
EXPECT_EQ([ReadMessage(handle0)[@"result"] unsignedIntValue],
MOJO_RESULT_SHOULD_WAIT);
// Write to the other end of the pipe.
WriteMessage(handle1, @"QUJDRA=="); // "ABCD" in base-64
// Check the watcher was reamed and works.
EXPECT_EQ(expected_script, WaitForLastJavaScriptCall());
CloseHandle(handle0);
CloseHandle(handle1);
}
TEST_F(MojoFacadeTest, CancelWatch) {
uint32_t handle0, handle1;
CreateMessagePipe(&handle0, &handle1);
// Make 2 watchers on one end of the pipe.
const int kCallbackId1 = 99;
const int kCallbackId2 = 101;
WatchHandle(handle0, kCallbackId1);
const int watch_id2 = WatchHandle(handle0, kCallbackId2);
const auto expected_script1 = base::StringPrintf(
"Mojo.internal.watchCallbacksHolder.callCallback(%d, %d)", kCallbackId1,
MOJO_RESULT_OK);
const auto expected_script2 = base::StringPrintf(
"Mojo.internal.watchCallbacksHolder.callCallback(%d, %d)", kCallbackId2,
MOJO_RESULT_OK);
// Write to the other end of the pipe.
WriteMessage(handle1, @"QUJDRA=="); // "ABCD" in base-64
// `expected_script1` is also called, but GetLastJavaScriptCall() will store
// only the last one.
EXPECT_EQ(expected_script2, WaitForLastJavaScriptCall());
// Read the pipe until MOJO_RESULT_SHOULD_WAIT is returned
// (the usual watcher behavior).
EXPECT_EQ([ReadMessage(handle0)[@"result"] unsignedIntValue], MOJO_RESULT_OK);
EXPECT_EQ([ReadMessage(handle0)[@"result"] unsignedIntValue],
MOJO_RESULT_SHOULD_WAIT);
// Cancel the second watcher and write again.
CancelWatch(handle0, watch_id2);
WriteMessage(handle1, @"QUJDRA=="); // "ABCD" in base-64
// Only the second watcher should be notified.
EXPECT_EQ(expected_script1, WaitForLastJavaScriptCall());
CloseHandle(handle0);
CloseHandle(handle1);
}
// Tests reading the message from the pipe.
TEST_F(MojoFacadeTest, ReadWrite) {
uint32_t handle0, handle1;
CreateMessagePipe(&handle0, &handle1);
// Write to the other end of the pipe.
WriteMessage(handle1, @"QUJDRA=="); // "ABCD" in base-64
// Read the message from the pipe.
NSDictionary* message = ReadMessage(handle0);
NSArray* expected_message =
@[ @65, @66, @67, @68 ]; // ASCII values for A, B, C, D
EXPECT_NSEQ(expected_message, message[@"buffer"]);
EXPECT_FALSE([message[@"handles"] count]);
EXPECT_EQ(MOJO_RESULT_OK, [message[@"result"] unsignedIntValue]);
CloseHandle(handle0);
CloseHandle(handle1);
}
} // namespace web