// Copyright 2015 The Weave 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 "src/privet/privet_handler.h" #include <set> #include <string> #include <utility> #include <base/bind.h> #include <base/json/json_reader.h> #include <base/json/json_writer.h> #include <base/strings/string_util.h> #include <base/values.h> #include <gmock/gmock.h> #include <gtest/gtest.h> #include <weave/test/unittest_utils.h> #include "src/privet/constants.h" #include "src/privet/mock_delegates.h" #include "src/test/mock_clock.h" using testing::_; using testing::DoAll; using testing::Invoke; using testing::Return; using testing::SetArgPointee; using testing::SaveArg; using testing::WithArgs; namespace weave { namespace privet { namespace { void LoadTestJson(const std::string& test_json, base::DictionaryValue* dictionary) { std::string json = test_json; base::ReplaceChars(json, "'", "\"", &json); int error = 0; std::string message; std::unique_ptr<base::Value> value( base::JSONReader::ReadAndReturnError(json, base::JSON_PARSE_RFC, &error, &message) .release()); EXPECT_TRUE(value.get()) << "\nError: " << message << "\n" << json; base::DictionaryValue* dictionary_ptr = nullptr; if (value->GetAsDictionary(&dictionary_ptr)) dictionary->MergeDictionary(dictionary_ptr); } struct CodeWithReason { CodeWithReason(int code_in, const std::string& reason_in) : code(code_in), reason(reason_in) {} int code; std::string reason; }; std::ostream& operator<<(std::ostream& stream, const CodeWithReason& error) { return stream << "{" << error.code << ", " << error.reason << "}"; } bool IsEqualError(const CodeWithReason& expected, const base::DictionaryValue& dictionary) { std::string reason; int code = 0; return dictionary.GetInteger("error.http_status", &code) && code == expected.code && dictionary.GetString("error.code", &reason) && reason == expected.reason; } // Some error sections in response JSON objects contained debugging information // which is of no interest for this test. So, remove the debug info from the // JSON before running validation logic on it. std::unique_ptr<base::DictionaryValue> StripDebugErrorDetails( const std::string& path_to_error_object, const base::DictionaryValue& value) { std::unique_ptr<base::DictionaryValue> result{value.DeepCopy()}; base::DictionaryValue* error_dict = nullptr; EXPECT_TRUE(result->GetDictionary(path_to_error_object, &error_dict)); scoped_ptr<base::Value> dummy; error_dict->RemovePath("error.debugInfo", &dummy); error_dict->RemovePath("error.message", &dummy); return result; } } // namespace class PrivetHandlerTest : public testing::Test { public: PrivetHandlerTest() {} protected: void SetUp() override { EXPECT_CALL(clock_, Now()) .WillRepeatedly(Return(base::Time::FromTimeT(1410000001))); auth_header_ = "Privet anonymous"; handler_.reset( new PrivetHandler(&cloud_, &device_, &security_, &wifi_, &clock_)); } const base::DictionaryValue& HandleRequest( const std::string& api, const base::DictionaryValue* input) { output_.Clear(); handler_->HandleRequest(api, auth_header_, input, base::Bind(&PrivetHandlerTest::HandlerCallback, base::Unretained(this))); return output_; } const base::DictionaryValue& HandleRequest(const std::string& api, const std::string& json_input) { base::DictionaryValue dictionary; LoadTestJson(json_input, &dictionary); return HandleRequest(api, &dictionary); } void HandleUnknownRequest(const std::string& api) { output_.Clear(); base::DictionaryValue dictionary; handler_->HandleRequest(api, auth_header_, &dictionary, base::Bind(&PrivetHandlerTest::HandlerNoFound)); } const base::DictionaryValue& GetResponse() const { return output_; } int GetResponseCount() const { return response_count_; } void SetNoWifiAndGcd() { handler_.reset( new PrivetHandler(&cloud_, &device_, &security_, nullptr, &clock_)); EXPECT_CALL(cloud_, GetCloudId()).WillRepeatedly(Return("")); EXPECT_CALL(cloud_, GetConnectionState()) .WillRepeatedly(ReturnRef(gcd_disabled_state_)); auto set_error = [](const std::string&, const std::string&, ErrorPtr* error) { Error::AddTo(error, FROM_HERE, "setupUnavailable", ""); }; EXPECT_CALL(cloud_, Setup(_, _, _)) .WillRepeatedly(DoAll(Invoke(set_error), Return(false))); } test::MockClock clock_; testing::StrictMock<MockCloudDelegate> cloud_; testing::StrictMock<MockDeviceDelegate> device_; testing::StrictMock<MockSecurityDelegate> security_; testing::StrictMock<MockWifiDelegate> wifi_; std::string auth_header_; private: void HandlerCallback(int status, const base::DictionaryValue& output) { output_.Clear(); ++response_count_; output_.MergeDictionary(&output); if (!output_.HasKey("error")) { EXPECT_EQ(200, status); return; } EXPECT_NE(200, status); output_.SetInteger("error.http_status", status); } static void HandlerNoFound(int status, const base::DictionaryValue&) { EXPECT_EQ(404, status); } std::unique_ptr<PrivetHandler> handler_; base::DictionaryValue output_; int response_count_{0}; ConnectionState gcd_disabled_state_{ConnectionState::kDisabled}; }; TEST_F(PrivetHandlerTest, UnknownApi) { HandleUnknownRequest("/privet/foo"); } TEST_F(PrivetHandlerTest, InvalidFormat) { auth_header_ = ""; EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidFormat"), HandleRequest("/privet/info", nullptr)); } TEST_F(PrivetHandlerTest, MissingAuth) { auth_header_ = ""; EXPECT_PRED2(IsEqualError, CodeWithReason(401, "missingAuthorization"), HandleRequest("/privet/info", "{}")); } TEST_F(PrivetHandlerTest, InvalidAuth) { auth_header_ = "foo"; EXPECT_PRED2(IsEqualError, CodeWithReason(401, "invalidAuthorization"), HandleRequest("/privet/info", "{}")); } TEST_F(PrivetHandlerTest, ExpiredAuth) { auth_header_ = "Privet 123"; EXPECT_CALL(security_, ParseAccessToken(_, _, _)) .WillRepeatedly(WithArgs<2>(Invoke([](ErrorPtr* error) { return Error::AddTo(error, FROM_HERE, "authorizationExpired", ""); }))); EXPECT_PRED2(IsEqualError, CodeWithReason(403, "authorizationExpired"), HandleRequest("/privet/info", "{}")); } TEST_F(PrivetHandlerTest, InvalidAuthScope) { EXPECT_PRED2(IsEqualError, CodeWithReason(403, "invalidAuthorizationScope"), HandleRequest("/privet/v3/setup/start", "{}")); } TEST_F(PrivetHandlerTest, InfoMinimal) { SetNoWifiAndGcd(); EXPECT_CALL(security_, GetPairingTypes()) .WillRepeatedly(Return(std::set<PairingType>{})); EXPECT_CALL(security_, GetCryptoTypes()) .WillRepeatedly(Return(std::set<CryptoType>{})); EXPECT_CALL(security_, GetAuthTypes()) .WillRepeatedly(Return(std::set<AuthType>{})); const char kExpected[] = R"({ 'version': '3.0', 'id': 'TestId', 'name': 'TestDevice', 'services': [ "developmentBoard" ], 'modelManifestId': "ABMID", 'basicModelManifest': { 'uiDeviceKind': 'developmentBoard', 'oemName': 'Chromium', 'modelName': 'Brillo' }, 'endpoints': { 'httpPort': 0, 'httpUpdatesPort': 0, 'httpsPort': 0, 'httpsUpdatesPort': 0 }, 'authentication': { 'anonymousMaxScope': 'user', 'mode': [ ], 'pairing': [ ], 'crypto': [ ] }, 'gcd': { 'id': '', 'status': 'disabled' }, 'time': 1410000001000.0, 'sessionId': 'SessionId' })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/info", "{}")); } TEST_F(PrivetHandlerTest, Info) { EXPECT_CALL(cloud_, GetDescription()) .WillRepeatedly(Return("TestDescription")); EXPECT_CALL(cloud_, GetLocation()).WillRepeatedly(Return("TestLocation")); EXPECT_CALL(device_, GetHttpEnpoint()) .WillRepeatedly(Return(std::make_pair(80, 10080))); EXPECT_CALL(device_, GetHttpsEnpoint()) .WillRepeatedly(Return(std::make_pair(443, 10443))); EXPECT_CALL(wifi_, GetHostedSsid()) .WillRepeatedly(Return("Test_device.BBABCLAprv")); const char kExpected[] = R"({ 'version': '3.0', 'id': 'TestId', 'name': 'TestDevice', 'description': 'TestDescription', 'location': 'TestLocation', 'services': [ "developmentBoard" ], 'modelManifestId': "ABMID", 'basicModelManifest': { 'uiDeviceKind': 'developmentBoard', 'oemName': 'Chromium', 'modelName': 'Brillo' }, 'endpoints': { 'httpPort': 80, 'httpUpdatesPort': 10080, 'httpsPort': 443, 'httpsUpdatesPort': 10443 }, 'authentication': { 'anonymousMaxScope': 'none', 'mode': [ 'anonymous', 'pairing', 'local' ], 'pairing': [ 'pinCode', 'embeddedCode' ], 'crypto': [ 'p224_spake2' ] }, 'wifi': { 'capabilities': [ '2.4GHz' ], 'ssid': 'TestSsid', 'hostedSsid': 'Test_device.BBABCLAprv', 'status': 'offline' }, 'gcd': { 'id': 'TestCloudId', 'status': 'online' }, 'time': 1410000001000.0, 'sessionId': 'SessionId' })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/info", "{}")); } TEST_F(PrivetHandlerTest, PairingStartInvalidParams) { EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidParams"), HandleRequest("/privet/v3/pairing/start", "{'pairing':'embeddedCode','crypto':'crypto'}")); EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidParams"), HandleRequest("/privet/v3/pairing/start", "{'pairing':'code','crypto':'p224_spake2'}")); } TEST_F(PrivetHandlerTest, PairingStart) { EXPECT_JSON_EQ( "{'deviceCommitment': 'testCommitment', 'sessionId': 'testSession'}", HandleRequest("/privet/v3/pairing/start", "{'pairing': 'embeddedCode', 'crypto': 'p224_spake2'}")); } TEST_F(PrivetHandlerTest, PairingConfirm) { EXPECT_JSON_EQ( "{'certFingerprint':'testFingerprint','certSignature':'testSignature'}", HandleRequest( "/privet/v3/pairing/confirm", "{'sessionId':'testSession','clientCommitment':'testCommitment'}")); } TEST_F(PrivetHandlerTest, PairingCancel) { EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/pairing/cancel", "{'sessionId': 'testSession'}")); } TEST_F(PrivetHandlerTest, AuthErrorNoType) { EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidAuthMode"), HandleRequest("/privet/v3/auth", "{}")); } TEST_F(PrivetHandlerTest, AuthErrorInvalidType) { EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidAuthMode"), HandleRequest("/privet/v3/auth", "{'mode':'unknown'}")); } TEST_F(PrivetHandlerTest, AuthErrorNoScope) { EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidRequestedScope"), HandleRequest("/privet/v3/auth", "{'mode':'anonymous'}")); } TEST_F(PrivetHandlerTest, AuthErrorInvalidScope) { EXPECT_PRED2( IsEqualError, CodeWithReason(400, "invalidRequestedScope"), HandleRequest("/privet/v3/auth", "{'mode':'anonymous','requestedScope':'unknown'}")); } TEST_F(PrivetHandlerTest, AuthErrorAccessDenied) { EXPECT_PRED2(IsEqualError, CodeWithReason(403, "accessDenied"), HandleRequest("/privet/v3/auth", "{'mode':'anonymous','requestedScope':'owner'}")); } TEST_F(PrivetHandlerTest, AuthErrorInvalidAuthCode) { auto set_error = [](ErrorPtr* error) { return Error::AddTo(error, FROM_HERE, "invalidAuthCode", ""); }; EXPECT_CALL(security_, CreateAccessToken(_, "testToken", _, _, _, _, _)) .WillRepeatedly(WithArgs<6>(Invoke(set_error))); const char kInput[] = R"({ 'mode': 'pairing', 'requestedScope': 'user', 'authCode': 'testToken' })"; EXPECT_PRED2(IsEqualError, CodeWithReason(403, "invalidAuthCode"), HandleRequest("/privet/v3/auth", kInput)); } TEST_F(PrivetHandlerTest, AuthAnonymous) { const char kExpected[] = R"({ 'accessToken': 'GuestAccessToken', 'expiresIn': 15, 'scope': 'viewer', 'tokenType': 'Privet' })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/auth", "{'mode':'anonymous','requestedScope':'auto'}")); } TEST_F(PrivetHandlerTest, AuthPairing) { EXPECT_CALL(security_, CreateAccessToken(_, _, _, _, _, _, _)) .WillRepeatedly(DoAll(SetArgPointee<3>("OwnerAccessToken"), SetArgPointee<4>(AuthScope::kOwner), SetArgPointee<5>(base::TimeDelta::FromSeconds(15)), Return(true))); const char kInput[] = R"({ 'mode': 'pairing', 'requestedScope': 'owner', 'authCode': 'testToken' })"; const char kExpected[] = R"({ 'accessToken': 'OwnerAccessToken', 'expiresIn': 15, 'scope': 'owner', 'tokenType': 'Privet' })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/auth", kInput)); } TEST_F(PrivetHandlerTest, AuthLocalAuto) { EXPECT_CALL(security_, CreateAccessToken(_, _, _, _, _, _, _)) .WillRepeatedly(DoAll(SetArgPointee<3>("UserAccessToken"), SetArgPointee<4>(AuthScope::kUser), SetArgPointee<5>(base::TimeDelta::FromSeconds(15)), Return(true))); const char kInput[] = R"({ 'mode': 'local', 'requestedScope': 'auto', 'authCode': 'localAuthToken' })"; const char kExpected[] = R"({ 'accessToken': 'UserAccessToken', 'expiresIn': 15, 'scope': 'user', 'tokenType': 'Privet' })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/auth", kInput)); } TEST_F(PrivetHandlerTest, AuthLocal) { EXPECT_CALL(security_, CreateAccessToken(_, _, _, _, _, _, _)) .WillRepeatedly(DoAll(SetArgPointee<3>("ManagerAccessToken"), SetArgPointee<4>(AuthScope::kManager), SetArgPointee<5>(base::TimeDelta::FromSeconds(15)), Return(true))); const char kInput[] = R"({ 'mode': 'local', 'requestedScope': 'manager', 'authCode': 'localAuthToken' })"; const char kExpected[] = R"({ 'accessToken': 'ManagerAccessToken', 'expiresIn': 15, 'scope': 'manager', 'tokenType': 'Privet' })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/auth", kInput)); } TEST_F(PrivetHandlerTest, AuthLocalHighScope) { EXPECT_CALL(security_, CreateAccessToken(_, _, _, _, _, _, _)) .WillRepeatedly(DoAll(SetArgPointee<3>("UserAccessToken"), SetArgPointee<4>(AuthScope::kUser), SetArgPointee<5>(base::TimeDelta::FromSeconds(1)), Return(true))); const char kInput[] = R"({ 'mode': 'local', 'requestedScope': 'manager', 'authCode': 'localAuthToken' })"; EXPECT_PRED2(IsEqualError, CodeWithReason(403, "accessDenied"), HandleRequest("/privet/v3/auth", kInput)); } class PrivetHandlerTestWithAuth : public PrivetHandlerTest { public: void SetUp() override { PrivetHandlerTest::SetUp(); auth_header_ = "Privet 123"; EXPECT_CALL(security_, ParseAccessToken(_, _, _)) .WillRepeatedly(DoAll( SetArgPointee<1>(UserInfo{AuthScope::kOwner, TestUserId{"1"}}), Return(true))); } }; class PrivetHandlerSetupTest : public PrivetHandlerTestWithAuth {}; TEST_F(PrivetHandlerSetupTest, StatusEmpty) { SetNoWifiAndGcd(); EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/setup/status", "{}")); } TEST_F(PrivetHandlerSetupTest, StatusWifi) { wifi_.setup_state_ = SetupState{SetupState::kSuccess}; const char kExpected[] = R"({ 'wifi': { 'ssid': 'TestSsid', 'status': 'success' } })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/setup/status", "{}")); } TEST_F(PrivetHandlerSetupTest, StatusWifiError) { ErrorPtr error; Error::AddTo(&error, FROM_HERE, "invalidPassphrase", ""); wifi_.setup_state_ = SetupState{std::move(error)}; const char kExpected[] = R"({ 'wifi': { 'status': 'error', 'error': { 'code': 'invalidPassphrase' } } })"; EXPECT_JSON_EQ(kExpected, *StripDebugErrorDetails( "wifi", HandleRequest("/privet/v3/setup/status", "{}"))); } TEST_F(PrivetHandlerSetupTest, StatusGcd) { cloud_.setup_state_ = SetupState{SetupState::kSuccess}; const char kExpected[] = R"({ 'gcd': { 'id': 'TestCloudId', 'status': 'success' } })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/setup/status", "{}")); } TEST_F(PrivetHandlerSetupTest, StatusGcdError) { ErrorPtr error; Error::AddTo(&error, FROM_HERE, "invalidTicket", ""); cloud_.setup_state_ = SetupState{std::move(error)}; const char kExpected[] = R"({ 'gcd': { 'status': 'error', 'error': { 'code': 'invalidTicket' } } })"; EXPECT_JSON_EQ(kExpected, *StripDebugErrorDetails( "gcd", HandleRequest("/privet/v3/setup/status", "{}"))); } TEST_F(PrivetHandlerSetupTest, SetupNameDescriptionLocation) { EXPECT_CALL(cloud_, UpdateDeviceInfo("testName", "testDescription", "testLocation")) .Times(1); const char kInput[] = R"({ 'name': 'testName', 'description': 'testDescription', 'location': 'testLocation' })"; EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/setup/start", kInput)); } TEST_F(PrivetHandlerSetupTest, InvalidParams) { const char kInputWifi[] = R"({ 'wifi': { 'ssid': '' } })"; EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidParams"), HandleRequest("/privet/v3/setup/start", kInputWifi)); const char kInputRegistration[] = R"({ 'gcd': { 'ticketId': '' } })"; EXPECT_PRED2(IsEqualError, CodeWithReason(400, "invalidParams"), HandleRequest("/privet/v3/setup/start", kInputRegistration)); } TEST_F(PrivetHandlerSetupTest, WifiSetupUnavailable) { SetNoWifiAndGcd(); EXPECT_PRED2(IsEqualError, CodeWithReason(400, "setupUnavailable"), HandleRequest("/privet/v3/setup/start", "{'wifi': {}}")); } TEST_F(PrivetHandlerSetupTest, WifiSetup) { const char kInput[] = R"({ 'wifi': { 'ssid': 'testSsid', 'passphrase': 'testPass' } })"; auto set_error = [](const std::string&, const std::string&, ErrorPtr* error) { return Error::AddTo(error, FROM_HERE, "deviceBusy", ""); }; EXPECT_CALL(wifi_, ConfigureCredentials(_, _, _)).WillOnce(Invoke(set_error)); EXPECT_PRED2(IsEqualError, CodeWithReason(503, "deviceBusy"), HandleRequest("/privet/v3/setup/start", kInput)); const char kExpected[] = R"({ 'wifi': { 'status': 'inProgress' } })"; wifi_.setup_state_ = SetupState{SetupState::kInProgress}; EXPECT_CALL(wifi_, ConfigureCredentials("testSsid", "testPass", _)) .WillOnce(Return(true)); EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/setup/start", kInput)); } TEST_F(PrivetHandlerSetupTest, GcdSetupUnavailable) { SetNoWifiAndGcd(); const char kInput[] = R"({ 'gcd': { 'ticketId': 'testTicket', 'user': 'testUser' } })"; EXPECT_PRED2(IsEqualError, CodeWithReason(400, "setupUnavailable"), HandleRequest("/privet/v3/setup/start", kInput)); } TEST_F(PrivetHandlerSetupTest, GcdSetup) { const char kInput[] = R"({ 'gcd': { 'ticketId': 'testTicket', 'user': 'testUser' } })"; auto set_error = [](const std::string&, const std::string&, ErrorPtr* error) { return Error::AddTo(error, FROM_HERE, "deviceBusy", ""); }; EXPECT_CALL(cloud_, Setup(_, _, _)).WillOnce(Invoke(set_error)); EXPECT_PRED2(IsEqualError, CodeWithReason(503, "deviceBusy"), HandleRequest("/privet/v3/setup/start", kInput)); const char kExpected[] = R"({ 'gcd': { 'status': 'inProgress' } })"; cloud_.setup_state_ = SetupState{SetupState::kInProgress}; EXPECT_CALL(cloud_, Setup("testTicket", "testUser", _)) .WillOnce(Return(true)); EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/setup/start", kInput)); } TEST_F(PrivetHandlerSetupTest, GcdSetupAsMaster) { EXPECT_CALL(security_, ParseAccessToken(_, _, _)) .WillRepeatedly(DoAll( SetArgPointee<1>(UserInfo{AuthScope::kManager, TestUserId{"1"}}), Return(true))); const char kInput[] = R"({ 'gcd': { 'ticketId': 'testTicket', 'user': 'testUser' } })"; EXPECT_PRED2(IsEqualError, CodeWithReason(403, "invalidAuthorizationScope"), HandleRequest("/privet/v3/setup/start", kInput)); } TEST_F(PrivetHandlerTestWithAuth, ClaimAccessControl) { EXPECT_JSON_EQ("{'clientToken': 'RootClientAuthToken'}", HandleRequest("/privet/v3/accessControl/claim", "{}")); } TEST_F(PrivetHandlerTestWithAuth, ConfirmAccessControl) { EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/accessControl/confirm", "{'clientToken': 'DerivedClientAuthToken'}")); } TEST_F(PrivetHandlerTestWithAuth, State) { EXPECT_JSON_EQ("{'state': {'test': {}}, 'fingerprint': '1'}", HandleRequest("/privet/v3/state", "{}")); cloud_.NotifyOnStateChanged(); EXPECT_JSON_EQ("{'state': {'test': {}}, 'fingerprint': '2'}", HandleRequest("/privet/v3/state", "{}")); } TEST_F(PrivetHandlerTestWithAuth, CommandsDefs) { EXPECT_JSON_EQ("{'commands': {'test':{}}, 'fingerprint': '1'}", HandleRequest("/privet/v3/commandDefs", "{}")); cloud_.NotifyOnTraitDefsChanged(); EXPECT_JSON_EQ("{'commands': {'test':{}}, 'fingerprint': '2'}", HandleRequest("/privet/v3/commandDefs", "{}")); } TEST_F(PrivetHandlerTestWithAuth, Traits) { EXPECT_JSON_EQ("{'traits': {'test': {}}, 'fingerprint': '1'}", HandleRequest("/privet/v3/traits", "{}")); cloud_.NotifyOnTraitDefsChanged(); EXPECT_JSON_EQ("{'traits': {'test': {}}, 'fingerprint': '2'}", HandleRequest("/privet/v3/traits", "{}")); } TEST_F(PrivetHandlerTestWithAuth, Components) { EXPECT_JSON_EQ("{'components': {'test': {}}, 'fingerprint': '1'}", HandleRequest("/privet/v3/components", "{}")); cloud_.NotifyOnComponentTreeChanged(); EXPECT_JSON_EQ("{'components': {'test': {}}, 'fingerprint': '2'}", HandleRequest("/privet/v3/components", "{}")); // State change will also change the components fingerprint. cloud_.NotifyOnStateChanged(); EXPECT_JSON_EQ("{'components': {'test': {}}, 'fingerprint': '3'}", HandleRequest("/privet/v3/components", "{}")); } TEST_F(PrivetHandlerTestWithAuth, ComponentsWithFiltersAndPaths) { const char kComponents[] = R"({ "comp1": { "traits": ["a", "b"], "state": { "a" : { "prop": 1 } }, "components": { "comp2": { "traits": ["c"], "components": { "comp4": { "traits": ["d"] } } }, "comp3": { "traits": ["e"] } } } })"; base::DictionaryValue components; LoadTestJson(kComponents, &components); EXPECT_CALL(cloud_, FindComponent(_, _)).WillRepeatedly(Return(nullptr)); EXPECT_CALL(cloud_, GetComponents()).WillRepeatedly(ReturnRef(components)); const char kExpected1[] = R"({ "components": { "comp1": { "state": { "a" : { "prop": 1 } } } }, "fingerprint": "1" })"; EXPECT_JSON_EQ(kExpected1, HandleRequest("/privet/v3/components", "{'filter':['state']}")); const char kExpected2[] = R"({ "components": { "comp1": { "traits": ["a", "b"] } }, "fingerprint": "1" })"; EXPECT_JSON_EQ(kExpected2, HandleRequest("/privet/v3/components", "{'filter':['traits']}")); const char kExpected3[] = R"({ "components": { "comp1": { "components": { "comp2": { "components": { "comp4": {} } }, "comp3": {} } } }, "fingerprint": "1" })"; EXPECT_JSON_EQ(kExpected3, HandleRequest("/privet/v3/components", "{'filter':['components']}")); const char kExpected4[] = R"({ "components": { "comp1": { "traits": ["a", "b"], "state": { "a" : { "prop": 1 } }, "components": { "comp2": { "traits": ["c"], "components": { "comp4": { "traits": ["d"] } } }, "comp3": { "traits": ["e"] } } } }, "fingerprint": "1" })"; EXPECT_JSON_EQ(kExpected4, HandleRequest("/privet/v3/components", "{'filter':['traits', 'components', 'state']}")); const base::DictionaryValue* comp2 = nullptr; ASSERT_TRUE(components.GetDictionary("comp1.components.comp2", &comp2)); EXPECT_CALL(cloud_, FindComponent("comp1.comp2", _)).WillOnce(Return(comp2)); const char kExpected5[] = R"({ "components": { "comp2": { "traits": ["c"], "components": { "comp4": { "traits": ["d"] } } } }, "fingerprint": "1" })"; EXPECT_JSON_EQ( kExpected5, HandleRequest( "/privet/v3/components", "{'path':'comp1.comp2', 'filter':['traits', 'components']}")); auto error_handler = [](ErrorPtr* error) -> const base::DictionaryValue* { return Error::AddTo(error, FROM_HERE, "componentNotFound", ""); }; EXPECT_CALL(cloud_, FindComponent("comp7", _)) .WillOnce(WithArgs<1>(Invoke(error_handler))); EXPECT_PRED2( IsEqualError, CodeWithReason(500, "componentNotFound"), HandleRequest("/privet/v3/components", "{'path':'comp7', 'filter':['traits', 'components']}")); } TEST_F(PrivetHandlerTestWithAuth, CommandsExecute) { const char kInput[] = "{'name': 'test'}"; base::DictionaryValue command; LoadTestJson(kInput, &command); LoadTestJson("{'id':'5'}", &command); EXPECT_CALL(cloud_, AddCommand(_, _, _)) .WillOnce(WithArgs<2>(Invoke( [&command](const CloudDelegate::CommandDoneCallback& callback) { callback.Run(command, nullptr); }))); EXPECT_JSON_EQ("{'name':'test', 'id':'5'}", HandleRequest("/privet/v3/commands/execute", kInput)); } TEST_F(PrivetHandlerTestWithAuth, CommandsStatus) { const char kInput[] = "{'id': '5'}"; base::DictionaryValue command; LoadTestJson(kInput, &command); LoadTestJson("{'name':'test'}", &command); EXPECT_CALL(cloud_, GetCommand(_, _, _)) .WillOnce(WithArgs<2>(Invoke( [&command](const CloudDelegate::CommandDoneCallback& callback) { callback.Run(command, nullptr); }))); EXPECT_JSON_EQ("{'name':'test', 'id':'5'}", HandleRequest("/privet/v3/commands/status", kInput)); ErrorPtr error; Error::AddTo(&error, FROM_HERE, "notFound", ""); EXPECT_CALL(cloud_, GetCommand(_, _, _)) .WillOnce(WithArgs<2>( Invoke([&error](const CloudDelegate::CommandDoneCallback& callback) { callback.Run({}, std::move(error)); }))); EXPECT_PRED2(IsEqualError, CodeWithReason(404, "notFound"), HandleRequest("/privet/v3/commands/status", "{'id': '15'}")); } TEST_F(PrivetHandlerTestWithAuth, CommandsCancel) { const char kExpected[] = "{'id': '5', 'name':'test', 'state':'cancelled'}"; base::DictionaryValue command; LoadTestJson(kExpected, &command); EXPECT_CALL(cloud_, CancelCommand(_, _, _)) .WillOnce(WithArgs<2>(Invoke( [&command](const CloudDelegate::CommandDoneCallback& callback) { callback.Run(command, nullptr); }))); EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/commands/cancel", "{'id': '8'}")); ErrorPtr error; Error::AddTo(&error, FROM_HERE, "notFound", ""); EXPECT_CALL(cloud_, CancelCommand(_, _, _)) .WillOnce(WithArgs<2>( Invoke([&error](const CloudDelegate::CommandDoneCallback& callback) { callback.Run({}, std::move(error)); }))); EXPECT_PRED2(IsEqualError, CodeWithReason(404, "notFound"), HandleRequest("/privet/v3/commands/cancel", "{'id': '11'}")); } TEST_F(PrivetHandlerTestWithAuth, CommandsList) { const char kExpected[] = R"({ 'commands' : [ {'id':'5', 'state':'cancelled'}, {'id':'15', 'state':'inProgress'} ]})"; base::DictionaryValue commands; LoadTestJson(kExpected, &commands); EXPECT_CALL(cloud_, ListCommands(_, _)) .WillOnce(WithArgs<1>(Invoke( [&commands](const CloudDelegate::CommandDoneCallback& callback) { callback.Run(commands, nullptr); }))); EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/commands/list", "{}")); } class PrivetHandlerCheckForUpdatesTest : public PrivetHandlerTestWithAuth {}; TEST_F(PrivetHandlerCheckForUpdatesTest, NoInput) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); cloud_.NotifyOnTraitDefsChanged(); cloud_.NotifyOnComponentTreeChanged(); cloud_.NotifyOnStateChanged(); const char kInput[] = "{}"; const char kExpected[] = R"({ 'commandsFingerprint': '2', 'stateFingerprint': '2', 'traitsFingerprint': '2', 'componentsFingerprint': '3' })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(1, GetResponseCount()); } TEST_F(PrivetHandlerCheckForUpdatesTest, AlreadyChanged) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); cloud_.NotifyOnTraitDefsChanged(); cloud_.NotifyOnComponentTreeChanged(); cloud_.NotifyOnStateChanged(); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; const char kExpected[] = R"({ 'commandsFingerprint': '2', 'stateFingerprint': '2', 'traitsFingerprint': '2', 'componentsFingerprint': '3' })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(1, GetResponseCount()); } TEST_F(PrivetHandlerCheckForUpdatesTest, LongPollCommands) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnTraitDefsChanged(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '2', 'stateFingerprint': '1', 'traitsFingerprint': '2', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); } TEST_F(PrivetHandlerCheckForUpdatesTest, LongPollTraits) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnTraitDefsChanged(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '2', 'stateFingerprint': '1', 'traitsFingerprint': '2', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); } TEST_F(PrivetHandlerCheckForUpdatesTest, LongPollState) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnStateChanged(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '2', 'traitsFingerprint': '1', 'componentsFingerprint': '2' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); } TEST_F(PrivetHandlerCheckForUpdatesTest, LongPollComponents) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnComponentTreeChanged(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '2' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); } TEST_F(PrivetHandlerCheckForUpdatesTest, LongPollIgnoreTraits) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); const char kInput[] = R"({ 'stateFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnTraitDefsChanged(); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnComponentTreeChanged(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '2', 'stateFingerprint': '1', 'traitsFingerprint': '2', 'componentsFingerprint': '2' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); } TEST_F(PrivetHandlerCheckForUpdatesTest, LongPollIgnoreState) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); const char kInput[] = R"({ 'commandsFingerprint': '1', 'traitsFingerprint': '1' })"; EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnStateChanged(); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnComponentTreeChanged(); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnTraitDefsChanged(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '2', 'stateFingerprint': '2', 'traitsFingerprint': '2', 'componentsFingerprint': '3' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); } TEST_F(PrivetHandlerCheckForUpdatesTest, InstantTimeout) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1', 'waitTimeout': 0 })"; const char kExpected[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ(kExpected, HandleRequest("/privet/v3/checkForUpdates", kInput)); } TEST_F(PrivetHandlerCheckForUpdatesTest, UserTimeout) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1', 'waitTimeout': 3 })"; base::Closure callback; EXPECT_CALL(device_, PostDelayedTask(_, _, base::TimeDelta::FromSeconds(3))) .WillOnce(SaveArg<1>(&callback)); EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); callback.Run(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); } TEST_F(PrivetHandlerCheckForUpdatesTest, ServerTimeout) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::FromMinutes(1))); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; base::Closure callback; EXPECT_CALL(device_, PostDelayedTask(_, _, base::TimeDelta::FromSeconds(50))) .WillOnce(SaveArg<1>(&callback)); EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); callback.Run(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); } TEST_F(PrivetHandlerCheckForUpdatesTest, VeryShortServerTimeout) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::FromSeconds(5))); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ(kInput, HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(1, GetResponseCount()); } TEST_F(PrivetHandlerCheckForUpdatesTest, ServerAndUserTimeout) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::FromMinutes(1))); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1', 'waitTimeout': 10 })"; base::Closure callback; EXPECT_CALL(device_, PostDelayedTask(_, _, base::TimeDelta::FromSeconds(10))) .WillOnce(SaveArg<1>(&callback)); EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); callback.Run(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); } TEST_F(PrivetHandlerCheckForUpdatesTest, ChangeBeforeTimeout) { EXPECT_CALL(device_, GetHttpRequestTimeout()) .WillOnce(Return(base::TimeDelta::Max())); const char kInput[] = R"({ 'commandsFingerprint': '1', 'stateFingerprint': '1', 'traitsFingerprint': '1', 'componentsFingerprint': '1', 'waitTimeout': 10 })"; base::Closure callback; EXPECT_CALL(device_, PostDelayedTask(_, _, base::TimeDelta::FromSeconds(10))) .WillOnce(SaveArg<1>(&callback)); EXPECT_JSON_EQ("{}", HandleRequest("/privet/v3/checkForUpdates", kInput)); EXPECT_EQ(0, GetResponseCount()); cloud_.NotifyOnTraitDefsChanged(); EXPECT_EQ(1, GetResponseCount()); const char kExpected[] = R"({ 'commandsFingerprint': '2', 'stateFingerprint': '1', 'traitsFingerprint': '2', 'componentsFingerprint': '1' })"; EXPECT_JSON_EQ(kExpected, GetResponse()); callback.Run(); EXPECT_EQ(1, GetResponseCount()); } } // namespace privet } // namespace weave