Commit 72aafbd9 authored by Oleh Lamzin's avatar Oleh Lamzin Committed by Commit Bot

MessagePipe: migrate browser tests from MediaApp

Migrate message pipe browser tests from MediaApp.

Bug: b:159927590
Change-Id: Icc3d4d759bce94a739a6fe0b56c85129c5b58b2b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2282597
Commit-Queue: Oleh Lamzin <lamzin@google.com>
Reviewed-by: default avatarTrent Apted <tapted@chromium.org>
Reviewed-by: default avatarGiovanni Ortuño Urquidi <ortuno@chromium.org>
Cr-Commit-Position: refs/heads/master@{#789231}
parent ae48b208
......@@ -406,19 +406,6 @@ function assertMatch(string, regex, opt_message = undefined) {
chai.assert.match(string, new RegExp(regex), opt_message);
}
/**
* Use to match error stack traces.
* @param {string} stackTrace the stacktrace
* @param {!Array<string>} regexLines a list of escaped regex compatible
* strings, used to compare with the stacktrace.
* @param {string=} opt_message logged if the assertion fails
*/
function assertMatchErrorStack(
stackTrace, regexLines, opt_message = undefined) {
const regex = `(.|\\n)*${regexLines.join('(.|\\n)*')}(.|\\n)*`;
assertMatch(stackTrace, regex, opt_message);
}
/**
* Returns the files loaded in the most recent call to `loadFiles()`.
* @return {!Promise<?Array<!mediaApp.AbstractFile>>}
......
......@@ -165,11 +165,6 @@ function installTestHandlers() {
// Turn off error rethrowing for tests so the test runner doesn't mark
// our error handling tests as failed.
parentMessagePipe.rethrowErrors = false;
// Handler that will always error for helping to test the message pipe
// itself.
parentMessagePipe.registerHandler('bad-handler', () => {
throw Error('This is an error');
});
parentMessagePipe.registerHandler('run-test-case', (data) => {
return runTestCase(/** @type{!TestMessageRunTestCase} */ (data));
......
......@@ -473,65 +473,6 @@ TEST_F('MediaAppUIBrowserTest', 'CanFullscreenVideo', async () => {
testDone();
});
// Tests that we receive an error if our message is unhandled.
TEST_F('MediaAppUIBrowserTest', 'ReceivesNoHandlerError', async () => {
guestMessagePipe.logClientError = error => console.log(JSON.stringify(error));
let caughtError = {};
try {
await guestMessagePipe.sendMessage('unknown-message');
} catch (error) {
caughtError = error;
}
assertEquals(caughtError.name, 'Error');
assertEquals(
caughtError.message,
'unknown-message: No handler registered for message type \'unknown-message\'');
assertMatchErrorStack(caughtError.stack, [
// Error stack of the test context.
'Error: unknown-message: No handler registered for message type \'unknown-message\'',
'at MessagePipe.sendMessage \\(chrome:',
'at async MediaAppUIBrowserTest.<anonymous>',
// Error stack of the untrusted context (guestMessagePipe) is appended.
'Error from chrome-untrusted:',
'Error: No handler registered for message type \'unknown-message\'',
'at MessagePipe.receiveMessage_ \\(chrome-untrusted:',
'at MessagePipe.messageListener_ \\(chrome-untrusted:',
]);
testDone();
});
// Tests that we receive an error if the handler fails.
TEST_F('MediaAppUIBrowserTest', 'ReceivesProxiedError', async () => {
guestMessagePipe.logClientError = error => console.log(JSON.stringify(error));
let caughtError = {};
try {
await guestMessagePipe.sendMessage('bad-handler');
} catch (error) {
caughtError = error;
}
assertEquals(caughtError.name, 'Error');
assertEquals(caughtError.message, 'bad-handler: This is an error');
assertMatchErrorStack(caughtError.stack, [
// Error stack of the test context.
'Error: bad-handler: This is an error',
'at MessagePipe.sendMessage \\(chrome:',
'at async MediaAppUIBrowserTest.<anonymous>',
// Error stack of the untrusted context (guestMessagePipe) is appended.
'Error from chrome-untrusted:',
'Error: This is an error',
'at guest_query_receiver.js',
'at MessagePipe.callHandlerForMessageType_ \\(chrome-untrusted:',
'at MessagePipe.receiveMessage_ \\(chrome-untrusted:',
'at MessagePipe.messageListener_ \\(chrome-untrusted:',
]);
testDone();
});
// Tests the IPC behind the implementation of ReceivedFile.overwriteOriginal()
// in the untrusted context. Ensures it correctly updates the file handle owned
// by the privileged context.
......@@ -579,8 +520,7 @@ TEST_F('MediaAppUIBrowserTest', 'OverwriteOriginalPickerFallback', async () => {
testDone();
});
// Tests `MessagePipe.sendMessage()` properly propagates errors and appends
// stacktraces.
// Tests `MessagePipe.sendMessage()` properly propagates errors.
TEST_F('MediaAppUIBrowserTest', 'CrossContextErrors', async () => {
// Prevent the trusted context throwing errors resulting JS errors.
guestMessagePipe.logClientError = error => console.log(JSON.stringify(error));
......@@ -611,17 +551,6 @@ TEST_F('MediaAppUIBrowserTest', 'CrossContextErrors', async () => {
assertEquals(caughtError.name, 'NotAllowedError');
assertEquals(caughtError.message, `test: overwrite-file: ${error.message}`);
assertMatchErrorStack(caughtError.stack, [
// Error stack of the untrusted & test context.
'at MessagePipe.sendMessage \\(chrome-untrusted:',
'at async ReceivedFile.overwriteOriginal \\(chrome-untrusted:',
'at async runTestQuery \\(guest_query_receiver',
'at async MessagePipe.callHandlerForMessageType_ \\(chrome-untrusted:',
// Error stack of the trusted context is appended.
'Error from chrome:',
'NotAllowedError: Fake NotAllowedError for CrossContextErrors test.',
'at MediaAppUIBrowserTest',
]);
testDone();
});
......
......@@ -16,23 +16,14 @@ js2gtest("browser_tests_js") {
defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
deps = [ ":browser_test_support" ]
data = [ "message_pipe_test.html" ]
}
source_set("browser_test_support") {
testonly = true
sources = [
"message_pipe_browsertest.cc",
"message_pipe_browsertest.h",
]
defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
deps = [
"//chrome/test:test_support_ui",
"//chromeos/components/web_applications/test:test_support",
deps = [ "//chromeos/components/web_applications/test:test_support" ]
data = [
"message_pipe.js",
"test_data/message_pipe_browsertest_trusted.html",
"test_data/message_pipe_browsertest_trusted.js",
"test_data/message_pipe_browsertest_untrusted.html",
"test_data/message_pipe_browsertest_untrusted.js",
]
}
......@@ -57,7 +48,11 @@ js_type_check("closure_compile_message_pipe_browsertest_js") {
js_library("message_pipe_browsertest_js") {
testonly = true
sources = [ "message_pipe_browsertest.js" ]
sources = [
"message_pipe_browsertest.js",
"test_data/message_pipe_browsertest_trusted.js",
"test_data/message_pipe_browsertest_untrusted.js",
]
externs_list = [
"//chromeos/components/web_applications/js2gtest_support.externs.js",
"//third_party/chaijs/externs/chai-3.5.js",
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/components/system_apps/public/js/message_pipe_browsertest.h"
#include "base/files/file_path.h"
namespace {
constexpr base::FilePath::CharType kRootDir[] =
FILE_PATH_LITERAL("chromeos/components/system_apps/public/js/");
} // namespace
MessagePipeBrowserTestBase::MessagePipeBrowserTestBase()
: JsLibraryTest(base::FilePath(kRootDir)) {}
MessagePipeBrowserTestBase::~MessagePipeBrowserTestBase() = default;
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROMEOS_COMPONENTS_SYSTEM_APPS_PUBLIC_JS_MESSAGE_PIPE_BROWSERTEST_H_
#define CHROMEOS_COMPONENTS_SYSTEM_APPS_PUBLIC_JS_MESSAGE_PIPE_BROWSERTEST_H_
#include "chromeos/components/web_applications/test/js_library_test.h"
class MessagePipeBrowserTestBase : public JsLibraryTest {
public:
MessagePipeBrowserTestBase();
~MessagePipeBrowserTestBase() override;
MessagePipeBrowserTestBase(const MessagePipeBrowserTestBase&) = delete;
MessagePipeBrowserTestBase& operator=(const MessagePipeBrowserTestBase&) =
delete;
};
#endif // CHROMEOS_COMPONENTS_SYSTEM_APPS_PUBLIC_JS_MESSAGE_PIPE_BROWSERTEST_H_
......@@ -3,16 +3,49 @@
// found in the LICENSE file.
/**
* @fileoverview Test suite for message_pipe.js
* @fileoverview Test suite for message_pipe.js.
*/
GEN('#include "chromeos/components/web_applications/test/js_library_test.h"');
GEN('#include "chromeos/components/system_apps/public/js/message_pipe_browsertest.h"');
GEN('#include "content/public/test/browser_test.h"');
/**
* Wraps `chai.assert.match` allowing tests to use `assertMatch`.
* @param {string} string the string to match
* @param {string} regex an escaped regex compatible string
* @param {string=} opt_message logged if the assertion fails
*/
function assertMatch(string, regex, opt_message = undefined) {
chai.assert.match(string, new RegExp(regex), opt_message);
}
/**
* Use to match error stack traces.
* @param {string} stackTrace the stacktrace
* @param {!Array<string>} regexLines a list of escaped regex compatible
* strings, used to compare with the stacktrace.
* @param {string=} opt_message logged if the assertion fails
*/
function assertMatchErrorStack(
stackTrace, regexLines, opt_message = undefined) {
const regex = `(.|\\n)*${regexLines.join('(.|\\n)*')}(.|\\n)*`;
assertMatch(stackTrace, regex, opt_message);
}
/**
* @param {string} messageType
* @param {!Object=} message
* @return {!Promise<!Object>}
*/
async function sendTestMessage(messageType, message = {}) {
await testMessageHandlersReady;
return untrustedMessagePipe.sendMessage(messageType, message);
}
var MessagePipeBrowserTest = class extends testing.Test {
/** @override */
get browsePreload() {
return 'chrome://system-app-test/message_pipe_test.html';
return 'chrome://system-app-test/test_data/message_pipe_browsertest_trusted.html';
}
/** @override */
......@@ -22,7 +55,7 @@ var MessagePipeBrowserTest = class extends testing.Test {
/** @override */
get typedefCppFixture() {
return 'MessagePipeBrowserTestBase';
return 'JsLibraryTest';
}
/** @override */
......@@ -31,6 +64,112 @@ var MessagePipeBrowserTest = class extends testing.Test {
}
};
TEST_F('MessagePipeBrowserTest', 'Empty', () => {
TEST_F('MessagePipeBrowserTest', 'ReceivesSuccessResponse', async () => {
const request = {'foo': 'bar'};
const response = await sendTestMessage('success-message', request);
assertDeepEquals(response, {'success': true, 'request': request});
testDone();
});
// Tests that we receive an error if our message is unhandled.
TEST_F('MessagePipeBrowserTest', 'ReceivesNoHandlerError', async () => {
untrustedMessagePipe.logClientError = error =>
console.log(JSON.stringify(error));
let caughtError = {};
try {
await sendTestMessage('unknown-message');
} catch (error) {
caughtError = error;
}
assertEquals(caughtError.name, 'Error');
assertEquals(
caughtError.message,
'unknown-message: No handler registered for message type \'unknown-message\'');
assertMatchErrorStack(caughtError.stack, [
// Error stack of the test context.
'Error: unknown-message: No handler registered for message type \'unknown-message\'',
'at MessagePipe.sendMessage \\(chrome://system-app-test/',
'at async MessagePipeBrowserTest.',
// Error stack of the untrusted context.
'Error from chrome-untrusted://system-app-test',
'Error: No handler registered for message type \'unknown-message\'',
'at MessagePipe.receiveMessage_ \\(chrome-untrusted://system-app-test/',
'at MessagePipe.messageListener_ \\(chrome-untrusted://system-app-test/'
]);
testDone();
});
// Tests that we receive an error if the handler fails.
TEST_F('MessagePipeBrowserTest', 'ReceivesProxiedError', async () => {
untrustedMessagePipe.logClientError = error =>
console.log(JSON.stringify(error));
let caughtError = {};
try {
await sendTestMessage('bad-handler');
} catch (error) {
caughtError = error;
}
assertEquals(caughtError.name, 'Error');
assertEquals(
caughtError.message, 'bad-handler: This is an error from untrusted');
assertMatchErrorStack(caughtError.stack, [
// Error stack of the test context.
'Error: bad-handler: This is an error from untrusted',
'at MessagePipe.sendMessage \\(chrome://system-app-test/',
'at async MessagePipeBrowserTest.',
// Error stack of the untrusted context.
'Error from chrome-untrusted://system-app-test',
'Error: This is an error from untrusted',
'at chrome-untrusted://system-app-test/test_data/message_pipe_browsertest_untrusted.js',
'at MessagePipe.callHandlerForMessageType_ \\(chrome-untrusted://system-app-test/',
'at MessagePipe.receiveMessage_ \\(chrome-untrusted://system-app-test/',
'at MessagePipe.messageListener_ \\(chrome-untrusted://system-app-test/'
]);
testDone();
});
// Tests `MessagePipe.sendMessage()` properly propagates errors and appends
// stacktraces.
TEST_F('MessagePipeBrowserTest', 'CrossContextErrors', async () => {
untrustedMessagePipe.logClientError = error =>
console.log(JSON.stringify(error));
untrustedMessagePipe.rethrowErrors = false;
untrustedMessagePipe.registerHandler('bad-handler', () => {
throw Error('This is an error from trusted');
});
let caughtError = {};
try {
await sendTestMessage('request-bad-handler');
} catch (e) {
caughtError = e;
}
assertEquals(caughtError.name, 'Error');
assertEquals(
caughtError.message,
'request-bad-handler: bad-handler: This is an error from trusted');
assertMatchErrorStack(caughtError.stack, [
// Error stack of the test context.
'Error: request-bad-handler: bad-handler: This is an error from trusted',
'at MessagePipe.sendMessage \\(chrome://system-app-test/',
'at async MessagePipeBrowserTest',
// Error stack of the untrusted context.
'Error from chrome-untrusted://system-app-test',
'Error: bad-handler: This is an error from trusted',
'at MessagePipe.sendMessage \\(chrome-untrusted://system-app-test/',
'at async MessagePipe.callHandlerForMessageType_ \\(chrome-untrusted://system-app-test/',
// Error stack of the trusted context.
'Error from chrome://system-app-test',
'Error: This is an error from trusted', 'at .*message_pipe_browsertest.js',
'at MessagePipe.callHandlerForMessageType_',
'at MessagePipe.receiveMessage_', 'at MessagePipe.messageListener_'
]);
testDone();
});
<!-- Copyright 2020 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<!DOCTYPE html>
<script src="chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js"></script>
<iframe src="chrome-untrusted://system-app-test/test_data/message_pipe_browsertest_untrusted.html"></iframe>
<script src="/message_pipe.js"></script>
<script src="/test_data/message_pipe_browsertest_trusted.js"></script>
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** A pipe through which we can send messages to the untrusted frame. */
const untrustedMessagePipe =
new MessagePipe('chrome-untrusted://system-app-test');
/**
* Promise that signals the guest is ready to receive test messages.
* @type {!Promise<undefined>}
*/
const testMessageHandlersReady = new Promise(resolve => {
window.addEventListener('DOMContentLoaded', () => {
untrustedMessagePipe.registerHandler('test-handlers-ready', resolve);
});
});
<!-- Copyright 2020 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<!DOCTYPE html>
<script src="chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js"></script>
<script src="/message_pipe.js"></script>
<script src="/test_data/message_pipe_browsertest_untrusted.js"></script>
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** A pipe through which we can send messages to the parent frame. */
const parentMessagePipe =
new MessagePipe('chrome://system-app-test', window.parent);
/**
* Tells the test driver the guest test message handlers are installed. This
* requires the test handler that receives the signal to be set up. The order
* that this occurs can not be guaranteed. So this function retries until the
* signal is handled, which requires the 'test-handlers-ready' handler to be
* registered in message_pipe_browsertest_trusted.js.
*/
async function signalTestHandlersReady() {
const EXPECTED_ERROR =
`No handler registered for message type 'test-handlers-ready'`;
while (true) {
try {
await parentMessagePipe.sendMessage('test-handlers-ready', {});
return;
} catch (/** @type {!GenericErrorResponse} */ e) {
if (e.message !== EXPECTED_ERROR) {
console.error('Unexpected error in signalTestHandlersReady', e);
return;
}
}
}
}
/** Installs the MessagePipe handlers for receiving test queries. */
function installTestHandlers() {
// Turn off error rethrowing for tests so the test runner doesn't mark
// our error handling tests as failed.
parentMessagePipe.rethrowErrors = false;
// Log errors, rather than send them to console.error. This allows the error
// handling tests to work correctly.
parentMessagePipe.logClientError = error =>
console.log(JSON.stringify(error));
parentMessagePipe.registerHandler('success-message', (message) => {
return {'success': true, 'request': message};
});
parentMessagePipe.registerHandler('bad-handler', () => {
throw Error('This is an error from untrusted');
});
parentMessagePipe.registerHandler('request-bad-handler', async () => {
return parentMessagePipe.sendMessage('bad-handler');
});
signalTestHandlersReady();
}
// Ensure content and all scripts have loaded before installing test handlers.
if (document.readyState !== 'complete') {
window.addEventListener('load', installTestHandlers);
} else {
installTestHandlers();
}
......@@ -17,24 +17,30 @@
#include "content/public/browser/web_ui_controller_factory.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/url_constants.h"
#include "services/network/public/mojom/content_security_policy.mojom.h"
#include "ui/webui/mojo_web_ui_controller.h"
#include "url/gurl.h"
namespace {
constexpr base::FilePath::CharType kRootDir[] =
FILE_PATH_LITERAL("chromeos/components/system_apps/public/js/");
constexpr char kSystemAppTestHost[] = "system-app-test";
constexpr char kSystemAppTestURL[] = "chrome://system-app-test";
constexpr char kUntrustedSystemAppTestURL[] =
"chrome-untrusted://system-app-test/";
bool IsSystemAppTestURL(const GURL& url) {
return url.SchemeIs(content::kChromeUIScheme) &&
url.host() == kSystemAppTestHost;
}
void HandleRequest(const base::FilePath& root_dir,
const std::string& url_path,
void HandleRequest(const std::string& url_path,
content::WebUIDataSource::GotDataCallback callback) {
base::FilePath path;
CHECK(base::PathService::Get(base::BasePathKey::DIR_SOURCE_ROOT, &path));
path = path.Append(root_dir);
path = path.Append(kRootDir);
path = path.AppendASCII(url_path.substr(0, url_path.find("?")));
std::string contents;
......@@ -43,37 +49,64 @@ void HandleRequest(const base::FilePath& root_dir,
CHECK(base::ReadFileToString(path, &contents)) << path.value();
}
scoped_refptr<base::RefCountedString> ref_contents(
new base::RefCountedString);
ref_contents->data() = contents;
std::move(callback).Run(ref_contents);
std::move(callback).Run(base::RefCountedString::TakeString(&contents));
}
void SetRequestFilterForDataSource(content::WebUIDataSource& data_source) {
data_source.SetRequestFilter(
base::BindRepeating([](const std::string& path) { return true; }),
base::BindRepeating(&HandleRequest));
}
content::WebUIDataSource* CreateTrustedSysemAppTestDataSource() {
auto* trusted_source = content::WebUIDataSource::Create(kSystemAppTestHost);
// We need a CSP override to be able to embed a chrome-untrusted:// iframe.
// TODO(crbug.com/1105408): use FrameSrc instead of ChildSrc.
std::string csp =
std::string("child-src ") + kUntrustedSystemAppTestURL + ";";
trusted_source->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ChildSrc, csp);
SetRequestFilterForDataSource(*trusted_source);
return trusted_source;
}
content::WebUIDataSource* CreateUntrustedSystemAppTestDataSource() {
auto* untrusted_source =
content::WebUIDataSource::Create(kUntrustedSystemAppTestURL);
untrusted_source->AddFrameAncestor(GURL(kSystemAppTestURL));
SetRequestFilterForDataSource(*untrusted_source);
return untrusted_source;
}
class JsLibraryTestWebUIController : public ui::MojoWebUIController {
public:
explicit JsLibraryTestWebUIController(const base::FilePath& root_dir,
content::WebUI* web_ui)
explicit JsLibraryTestWebUIController(content::WebUI* web_ui)
: ui::MojoWebUIController(web_ui) {
auto* data_source = content::WebUIDataSource::Create(kSystemAppTestHost);
data_source->SetRequestFilter(
base::BindRepeating([](const std::string& path) { return true; }),
base::BindRepeating(&HandleRequest, root_dir));
auto* browser_context = web_ui->GetWebContents()->GetBrowserContext();
content::WebUIDataSource::Add(browser_context,
CreateTrustedSysemAppTestDataSource());
content::WebUIDataSource::Add(browser_context,
CreateUntrustedSystemAppTestDataSource());
content::WebUIDataSource::Add(web_ui->GetWebContents()->GetBrowserContext(),
data_source);
// Add ability to request chrome-untrusted: URLs
web_ui->AddRequestableScheme(content::kChromeUIUntrustedScheme);
}
};
class JsLibraryTestWebUIControllerFactory
: public content::WebUIControllerFactory {
public:
explicit JsLibraryTestWebUIControllerFactory(const base::FilePath& root_dir)
: root_dir_(root_dir) {}
JsLibraryTestWebUIControllerFactory() = default;
~JsLibraryTestWebUIControllerFactory() override = default;
std::unique_ptr<content::WebUIController> CreateWebUIControllerForURL(
content::WebUI* web_ui,
const GURL& url) override {
return std::make_unique<JsLibraryTestWebUIController>(root_dir_, web_ui);
return std::make_unique<JsLibraryTestWebUIController>(web_ui);
}
content::WebUI::TypeID GetWebUIType(content::BrowserContext* browser_context,
......@@ -93,16 +126,12 @@ class JsLibraryTestWebUIControllerFactory
const GURL& url) override {
return IsSystemAppTestURL(url);
}
private:
const base::FilePath root_dir_;
};
} // namespace
JsLibraryTest::JsLibraryTest(const base::FilePath& root_dir)
: factory_(
std::make_unique<JsLibraryTestWebUIControllerFactory>(root_dir)) {
JsLibraryTest::JsLibraryTest()
: factory_(std::make_unique<JsLibraryTestWebUIControllerFactory>()) {
content::WebUIControllerFactory::RegisterFactory(factory_.get());
}
......
......@@ -9,19 +9,16 @@
#include "chrome/test/base/mojo_web_ui_browser_test.h"
namespace base {
class FilePath;
} // namespace base
namespace content {
class WebUIControllerFactory;
} // namespace content
// Base test class used to test JS libraries for System Apps. It runs tests from
// chrome://system-app-test and loads files from |root_dir|.
// Base test class used to test JS libraries for System Apps. It setups
// chrome://system-app-test and chrome-untrusted://system-app-test URLs and
// loads files from chromeos/components/system_apps/public/js/.
class JsLibraryTest : public MojoWebUIBrowserTest {
public:
explicit JsLibraryTest(const base::FilePath& root_dir);
JsLibraryTest();
~JsLibraryTest() override;
JsLibraryTest(const JsLibraryTest&) = delete;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment