Danil Somsikov | da8c570 | 2023-08-29 11:36:23 | [diff] [blame] | 1 | // Copyright 2023 The Chromium Authors |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "chrome/browser/devtools/aida_client.h" |
| 6 | |
| 7 | #include <memory> |
| 8 | #include <utility> |
| 9 | |
| 10 | #include "base/functional/bind.h" |
Danil Somsikov | 4b8fdc1 | 2024-02-07 10:01:41 | [diff] [blame] | 11 | #include "base/test/metrics/histogram_tester.h" |
Danil Somsikov | da8c570 | 2023-08-29 11:36:23 | [diff] [blame] | 12 | #include "chrome/browser/signin/identity_test_environment_profile_adaptor.h" |
| 13 | #include "chrome/test/base/testing_profile.h" |
| 14 | #include "content/public/browser/network_service_instance.h" |
| 15 | #include "content/public/test/browser_task_environment.h" |
| 16 | #include "content/public/test/test_utils.h" |
| 17 | #include "net/test/embedded_test_server/embedded_test_server.h" |
| 18 | #include "services/network/network_service.h" |
John Abd-El-Malek | f2592db | 2024-02-12 07:20:23 | [diff] [blame] | 19 | #include "services/network/public/mojom/network_context_client.mojom.h" |
Danil Somsikov | da8c570 | 2023-08-29 11:36:23 | [diff] [blame] | 20 | #include "services/network/test/test_shared_url_loader_factory.h" |
| 21 | #include "testing/gtest/include/gtest/gtest.h" |
| 22 | |
| 23 | namespace { |
| 24 | using net::test_server::BasicHttpResponse; |
| 25 | using net::test_server::HttpRequest; |
| 26 | using net::test_server::HttpResponse; |
| 27 | } // namespace |
| 28 | |
| 29 | const char kEmail[] = "alice@example.com"; |
| 30 | const char kEndpointPath[] = "/foo"; |
| 31 | const char kScope[] = "bar"; |
| 32 | const char kRequest[] = |
| 33 | R"({"input": "What does this code do: 1+1", "client": "GENERAL"})"; |
| 34 | const char kResponse[] = |
| 35 | R"([{"textChunk":{"text":"The function `foo()` takes no arguments and returns nothing."}}])"; |
| 36 | |
| 37 | class AidaClientTest : public testing::Test { |
| 38 | public: |
| 39 | AidaClientTest() |
| 40 | : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP), |
| 41 | profile_(IdentityTestEnvironmentProfileAdaptor:: |
| 42 | CreateProfileForIdentityTestEnvironment()), |
| 43 | identity_test_env_adaptor_( |
| 44 | std::make_unique<IdentityTestEnvironmentProfileAdaptor>( |
| 45 | profile_.get())), |
| 46 | identity_test_env_(identity_test_env_adaptor_->identity_test_env()) { |
| 47 | content::GetNetworkService(); |
| 48 | content::RunAllPendingInMessageLoop(content::BrowserThread::IO); |
| 49 | |
| 50 | shared_url_loader_factory_ = |
| 51 | base::MakeRefCounted<network::TestSharedURLLoaderFactory>( |
| 52 | network::NetworkService::GetNetworkServiceForTesting()); |
| 53 | |
| 54 | identity_test_env_->MakePrimaryAccountAvailable( |
| 55 | kEmail, signin::ConsentLevel::kSync); |
| 56 | } |
| 57 | |
| 58 | protected: |
| 59 | content::BrowserTaskEnvironment task_environment_; |
| 60 | std::unique_ptr<network::mojom::NetworkContextClient> network_context_client_; |
| 61 | scoped_refptr<network::TestSharedURLLoaderFactory> shared_url_loader_factory_; |
| 62 | net::EmbeddedTestServer test_server_; |
| 63 | std::unique_ptr<TestingProfile> profile_; |
| 64 | std::unique_ptr<IdentityTestEnvironmentProfileAdaptor> |
| 65 | identity_test_env_adaptor_; |
| 66 | signin::IdentityTestEnvironment* identity_test_env_; |
Danil Somsikov | 4b8fdc1 | 2024-02-07 10:01:41 | [diff] [blame] | 67 | base::HistogramTester histogram_tester_; |
Danil Somsikov | da8c570 | 2023-08-29 11:36:23 | [diff] [blame] | 68 | }; |
| 69 | |
| 70 | class Delegate { |
| 71 | public: |
| 72 | Delegate() = default; |
| 73 | |
| 74 | std::unique_ptr<HttpResponse> HandleRequest(const HttpRequest& request) { |
| 75 | request_ = request.content; |
| 76 | authorization_header_ = |
| 77 | request.headers.at(net::HttpRequestHeaders::kAuthorization); |
| 78 | |
| 79 | auto http_response = std::make_unique<BasicHttpResponse>(); |
| 80 | http_response->set_code(api_response_code_); |
| 81 | http_response->set_content(api_response_); |
| 82 | http_response->set_content_type("application/json"); |
| 83 | return http_response; |
| 84 | } |
| 85 | |
| 86 | void FinishCallback(base::RunLoop* run_loop, const std::string& response) { |
| 87 | response_ = response; |
| 88 | if (run_loop) { |
| 89 | run_loop->Quit(); |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | std::string request_; |
| 94 | std::string api_response_ = kResponse; |
| 95 | net::HttpStatusCode api_response_code_ = net::HTTP_OK; |
| 96 | std::string response_; |
| 97 | std::string authorization_header_; |
| 98 | }; |
| 99 | |
| 100 | constexpr char kOAuthToken[] = "5678"; |
| 101 | |
| 102 | TEST_F(AidaClientTest, DoesNothingIfNoScope) { |
| 103 | Delegate delegate; |
| 104 | test_server_.RegisterRequestHandler(base::BindRepeating( |
| 105 | &Delegate::HandleRequest, base::Unretained(&delegate))); |
| 106 | |
| 107 | AidaClient aida_client(profile_.get(), shared_url_loader_factory_); |
| 108 | aida_client.OverrideAidaEndpointAndScopeForTesting("", ""); |
| 109 | aida_client.DoConversation( |
| 110 | kRequest, base::BindOnce(&Delegate::FinishCallback, |
| 111 | base::Unretained(&delegate), nullptr)); |
| 112 | EXPECT_EQ("", delegate.request_); |
| 113 | EXPECT_EQ(R"([{"error": "AIDA scope is not configured"}])", |
| 114 | delegate.response_); |
| 115 | } |
| 116 | |
| 117 | TEST_F(AidaClientTest, FailsIfNotAuthorized) { |
| 118 | base::RunLoop run_loop; |
| 119 | Delegate delegate; |
| 120 | |
| 121 | AidaClient aida_client(profile_.get(), shared_url_loader_factory_); |
| 122 | aida_client.OverrideAidaEndpointAndScopeForTesting("https://example.com/foo", |
| 123 | kScope); |
| 124 | aida_client.DoConversation( |
| 125 | kRequest, base::BindOnce(&Delegate::FinishCallback, |
| 126 | base::Unretained(&delegate), &run_loop)); |
| 127 | identity_test_env_->WaitForAccessTokenRequestIfNecessaryAndRespondWithError( |
| 128 | GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED)); |
| 129 | |
| 130 | EXPECT_EQ("", delegate.request_); |
| 131 | EXPECT_EQ( |
| 132 | R"([{"error": "Cannot get OAuth credentials", "detail": "Request canceled."}])", |
| 133 | delegate.response_); |
| 134 | } |
| 135 | |
| 136 | TEST_F(AidaClientTest, Succeeds) { |
| 137 | base::RunLoop run_loop; |
| 138 | Delegate delegate; |
| 139 | test_server_.RegisterRequestHandler(base::BindRepeating( |
| 140 | &Delegate::HandleRequest, base::Unretained(&delegate))); |
| 141 | |
| 142 | ASSERT_TRUE(test_server_.Start()); |
| 143 | |
| 144 | GURL endpoint_url = test_server_.GetURL(kEndpointPath); |
| 145 | AidaClient aida_client(profile_.get(), shared_url_loader_factory_); |
| 146 | aida_client.OverrideAidaEndpointAndScopeForTesting(endpoint_url.spec(), |
| 147 | kScope); |
| 148 | aida_client.DoConversation( |
| 149 | kRequest, base::BindOnce(&Delegate::FinishCallback, |
| 150 | base::Unretained(&delegate), &run_loop)); |
| 151 | identity_test_env_ |
| 152 | ->WaitForAccessTokenRequestIfNecessaryAndRespondWithTokenForScopes( |
| 153 | kOAuthToken, base::Time::Now() + base::Seconds(10), |
| 154 | std::string() /*id_token*/, signin::ScopeSet{kScope}); |
| 155 | run_loop.Run(); |
| 156 | |
| 157 | EXPECT_EQ(kRequest, delegate.request_); |
| 158 | EXPECT_EQ(kResponse, delegate.response_); |
Danil Somsikov | 4b8fdc1 | 2024-02-07 10:01:41 | [diff] [blame] | 159 | histogram_tester_.ExpectTotalCount("DevTools.AidaResponseTime", 1); |
Danil Somsikov | da8c570 | 2023-08-29 11:36:23 | [diff] [blame] | 160 | } |
| 161 | |
| 162 | TEST_F(AidaClientTest, ReusesOAuthToken) { |
| 163 | base::RunLoop run_loop; |
| 164 | Delegate delegate; |
| 165 | test_server_.RegisterRequestHandler(base::BindRepeating( |
| 166 | &Delegate::HandleRequest, base::Unretained(&delegate))); |
| 167 | |
| 168 | ASSERT_TRUE(test_server_.Start()); |
| 169 | |
| 170 | GURL endpoint_url = test_server_.GetURL(kEndpointPath); |
| 171 | AidaClient aida_client(profile_.get(), shared_url_loader_factory_); |
| 172 | aida_client.OverrideAidaEndpointAndScopeForTesting(endpoint_url.spec(), |
| 173 | kScope); |
| 174 | aida_client.DoConversation( |
| 175 | kRequest, base::BindOnce(&Delegate::FinishCallback, |
| 176 | base::Unretained(&delegate), &run_loop)); |
| 177 | identity_test_env_ |
| 178 | ->WaitForAccessTokenRequestIfNecessaryAndRespondWithTokenForScopes( |
| 179 | kOAuthToken, base::Time::Now() + base::Seconds(10), |
| 180 | std::string() /*id_token*/, signin::ScopeSet{kScope}); |
| 181 | run_loop.Run(); |
| 182 | |
| 183 | EXPECT_EQ(kRequest, delegate.request_); |
| 184 | EXPECT_EQ(kResponse, delegate.response_); |
| 185 | std::string authorization_header = delegate.authorization_header_; |
| 186 | |
| 187 | const char kAnotherRequest[] = "another request"; |
| 188 | const char kAnotherResponse[] = "another response"; |
| 189 | delegate.api_response_ = kAnotherResponse; |
| 190 | base::RunLoop run_loop2; |
| 191 | aida_client.DoConversation( |
| 192 | kAnotherRequest, base::BindOnce(&Delegate::FinishCallback, |
| 193 | base::Unretained(&delegate), &run_loop2)); |
| 194 | run_loop2.Run(); |
| 195 | EXPECT_EQ(kAnotherRequest, delegate.request_); |
| 196 | EXPECT_EQ(kAnotherResponse, delegate.response_); |
| 197 | EXPECT_EQ(authorization_header, delegate.authorization_header_); |
| 198 | } |
| 199 | |
| 200 | TEST_F(AidaClientTest, RefetchesTokenIfUnauthorized) { |
| 201 | base::RunLoop run_loop; |
| 202 | Delegate delegate; |
| 203 | test_server_.RegisterRequestHandler(base::BindRepeating( |
| 204 | &Delegate::HandleRequest, base::Unretained(&delegate))); |
| 205 | |
| 206 | ASSERT_TRUE(test_server_.Start()); |
| 207 | |
| 208 | GURL endpoint_url = test_server_.GetURL(kEndpointPath); |
| 209 | AidaClient aida_client(profile_.get(), shared_url_loader_factory_); |
| 210 | aida_client.OverrideAidaEndpointAndScopeForTesting(endpoint_url.spec(), |
| 211 | kScope); |
| 212 | aida_client.DoConversation( |
| 213 | kRequest, base::BindOnce(&Delegate::FinishCallback, |
| 214 | base::Unretained(&delegate), &run_loop)); |
| 215 | identity_test_env_ |
| 216 | ->WaitForAccessTokenRequestIfNecessaryAndRespondWithTokenForScopes( |
| 217 | kOAuthToken, base::Time::Now() + base::Seconds(10), |
| 218 | std::string() /*id_token*/, signin::ScopeSet{kScope}); |
| 219 | run_loop.Run(); |
| 220 | |
| 221 | EXPECT_EQ(kRequest, delegate.request_); |
| 222 | EXPECT_EQ(kResponse, delegate.response_); |
| 223 | std::string authorization_header = delegate.authorization_header_; |
| 224 | |
| 225 | delegate.api_response_code_ = net::HTTP_UNAUTHORIZED; |
| 226 | base::RunLoop run_loop2; |
| 227 | const char kAnotherRequest[] = "another request"; |
| 228 | const char kAnotherResponse[] = "another response"; |
| 229 | const char kAnotherOAuthToken[] = "another token"; |
| 230 | |
| 231 | aida_client.DoConversation( |
| 232 | kAnotherRequest, base::BindOnce(&Delegate::FinishCallback, |
| 233 | base::Unretained(&delegate), &run_loop2)); |
| 234 | identity_test_env_ |
| 235 | ->WaitForAccessTokenRequestIfNecessaryAndRespondWithTokenForScopes( |
| 236 | kAnotherOAuthToken, base::Time::Now() + base::Seconds(10), |
| 237 | std::string() /*id_token*/, signin::ScopeSet{kScope}); |
| 238 | delegate.api_response_code_ = net::HTTP_OK; |
| 239 | delegate.api_response_ = kAnotherResponse; |
| 240 | |
| 241 | run_loop2.Run(); |
| 242 | EXPECT_EQ(kAnotherRequest, delegate.request_); |
| 243 | EXPECT_EQ(kAnotherResponse, delegate.response_); |
| 244 | EXPECT_NE(authorization_header, delegate.authorization_header_); |
| 245 | } |