1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ios/web/net/clients/crw_js_injection_network_client.h"
7 #import <Foundation/Foundation.h>
9 #include "base/files/file_path.h"
10 #include "base/files/file_util.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/path_service.h"
13 #include "base/strings/sys_string_conversions.h"
14 #import "ios/net/clients/crn_network_client_protocol.h"
15 #import "ios/net/crn_http_url_response.h"
16 #include "testing/gmock/include/gmock/gmock.h"
17 #include "testing/gtest/include/gtest/gtest.h"
18 #import "third_party/ocmock/OCMock/OCMock.h"
19 #import "third_party/ocmock/gtest_support.h"
23 // Returns an NSString filled with the char 'a' of length |length|.
24 NSString* GetLongString(NSUInteger length) {
25 base::scoped_nsobject<NSMutableData> data(
26 [[NSMutableData alloc] initWithLength:length]);
27 memset([data mutableBytes], 'a', length);
28 NSString* long_string =
29 [[NSString alloc] initWithData:data
30 encoding:NSASCIIStringEncoding];
31 return [long_string autorelease];
36 // Class to serve as underlying client for JS injection client to expose
37 // data and responses that are passed on from the JS injection client.
38 @interface UnderlyingClient : CRNForwardingNetworkClient {
39 base::scoped_nsobject<NSMutableData> _loadedData;
40 base::scoped_nsobject<NSURLResponse> _receivedResponse;
42 // Returns all data loaded by the client.
43 - (NSData*)loadedData;
44 // Returns response received by the client.
45 - (NSURLResponse*)receivedResponse;
48 @implementation UnderlyingClient
50 - (instancetype)init {
51 if ((self = [super init])) {
52 _loadedData.reset([[NSMutableData alloc] init]);
57 - (NSData*)loadedData {
58 return _loadedData.get();
61 - (NSURLResponse*)receivedResponse {
62 return _receivedResponse.get();
65 - (void)didLoadData:(NSData*)data {
66 [_loadedData appendData:data];
67 [super didLoadData:data];
70 - (void)didReceiveResponse:(NSURLResponse*)response {
71 _receivedResponse.reset([response copy]);
72 [super didReceiveResponse:response];
79 const char kTestFile[] = "ios/web/test/data/chrome.html";
81 class CRWJSInjectionNetworkClientTest : public testing::Test {
83 CRWJSInjectionNetworkClientTest() {}
85 void SetUp() override {
86 // Set up mock original network client proxy.
87 mock_web_proxy_.reset([[OCMockObject
88 niceMockForProtocol:@protocol(CRNNetworkClientProtocol)] retain]);
90 // Set up underlying client to inspect data and responses passed on by
91 // the JS injection client.
92 underlying_client_.reset([[UnderlyingClient alloc] init]);
93 [underlying_client_ setUnderlyingClient:
94 static_cast<id<CRNNetworkClientProtocol>>(mock_web_proxy_)];
96 // Link mock proxy into the JSInjectionNetworkClient.
97 js_injection_client_.reset([[CRWJSInjectionNetworkClient alloc] init]);
98 [js_injection_client_ setUnderlyingClient:underlying_client_];
100 // Load data for testing
101 base::FilePath file_path;
102 ASSERT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &file_path));
103 file_path = file_path.AppendASCII(kTestFile);
104 test_data_.reset([[NSData dataWithContentsOfFile:
105 base::SysUTF8ToNSString(file_path.value())] retain]);
106 ASSERT_TRUE(test_data_);
109 void TearDown() override { EXPECT_OCMOCK_VERIFY(mock_web_proxy_); }
112 // Returns a CRNHTTPURLResponse. If |include_content_length|, header includes
113 // Content-Length set to the length of test_data_.
114 CRNHTTPURLResponse* CreateTestResponse(BOOL include_content_length);
116 // Returns number of times an injected cr_web script tag is found in the
117 // underlying client's loaded data. Script tag should immediately follow
118 // the html start tag, if it exists, or should be injected before any header,
119 // if the first tag is something other than an html start tag.
120 NSUInteger GetScriptTagCount() const;
122 // Checks that if response forwarded to the underlying client has header field
123 // Content-Length, the value matches the length of the data.
124 void ExpectConsistentContentLength();
126 base::scoped_nsobject<CRWJSInjectionNetworkClient> js_injection_client_;
127 base::scoped_nsobject<UnderlyingClient> underlying_client_;
128 base::scoped_nsobject<OCMockObject> mock_web_proxy_;
129 base::scoped_nsobject<NSData> test_data_;
132 CRNHTTPURLResponse* CRWJSInjectionNetworkClientTest::CreateTestResponse(
133 BOOL include_content_length) {
134 NSMutableDictionary *headers = [NSMutableDictionary
135 dictionaryWithDictionary:@{ @"Content-Type" : @"text/html" }];
136 if (include_content_length) {
137 headers[@"Content-Length"] = @([test_data_ length]).stringValue;
139 return [[CRNHTTPURLResponse alloc]
140 initWithURL:[NSURL URLWithString:@"http://testjsinjection.html"]
142 HTTPVersion:@"HTTP/1.1"
143 headerFields:headers];
146 NSUInteger CRWJSInjectionNetworkClientTest::GetScriptTagCount() const {
147 base::scoped_nsobject<NSString> data_string(
148 [[NSString alloc] initWithData:[underlying_client_ loadedData]
149 encoding:NSUTF8StringEncoding]);
150 NSRegularExpression* script_tag_reg_exp = [NSRegularExpression
151 regularExpressionWithPattern:@"(^|<html>)<script src="
152 "\"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-"
153 "[0-9a-f]{4}-[0-9a-f]{12}+_crweb\\.js\" "
154 "charset=\"utf-8\"></script>"
155 options:NSRegularExpressionCaseInsensitive
157 return [script_tag_reg_exp
158 numberOfMatchesInString:data_string
160 range:NSMakeRange(0, [data_string length])];
163 void CRWJSInjectionNetworkClientTest::ExpectConsistentContentLength() {
164 NSData* forwarded_data = [underlying_client_ loadedData];
165 NSDictionary* output_headers =
166 [static_cast<NSHTTPURLResponse*>([underlying_client_ receivedResponse])
168 ASSERT_TRUE(output_headers);
169 NSInteger content_length = [output_headers[@"Content-Length"] integerValue];
170 if (content_length) {
171 EXPECT_EQ(static_cast<NSInteger>([forwarded_data length]),
180 // Tests injection where response header has Content-Length. Checks that
181 // Content-Length is updated to match new size of data.
182 TEST_F(CRWJSInjectionNetworkClientTest, InjectionWithContentLength) {
183 base::scoped_nsobject<CRNHTTPURLResponse> test_response(
184 CreateTestResponse(YES));
185 [js_injection_client_ didReceiveResponse:test_response];
186 [js_injection_client_ didLoadData:test_data_];
187 [js_injection_client_ didFinishLoading];
189 EXPECT_EQ(1u, GetScriptTagCount());
190 ExpectConsistentContentLength();
193 // Tests injection where response header does not have Content-Length.
194 TEST_F(CRWJSInjectionNetworkClientTest, InjectionWithoutContentLength) {
195 base::scoped_nsobject<CRNHTTPURLResponse> test_response(
196 CreateTestResponse(NO));
197 [js_injection_client_ didReceiveResponse:test_response];
198 [js_injection_client_ didLoadData:test_data_];
199 [js_injection_client_ didFinishLoading];
201 EXPECT_EQ(1u, GetScriptTagCount());
202 ExpectConsistentContentLength();
205 // Tests that injection occurs at the beginning of the file if data has no html
206 // start tag but does have other tags within the first 1KB.
207 TEST_F(CRWJSInjectionNetworkClientTest, MissingHTMLTag) {
208 base::scoped_nsobject<NSString> test_string(
209 [[NSString alloc] initWithData:test_data_
210 encoding:NSUTF8StringEncoding]);
211 NSData* truncated_data =
212 [[test_string stringByReplacingOccurrencesOfString:@"<html>"
214 dataUsingEncoding:NSUTF8StringEncoding];
215 test_data_.reset([truncated_data retain]);
216 ASSERT_TRUE(test_data_);
218 base::scoped_nsobject<CRNHTTPURLResponse> test_response(
219 CreateTestResponse(YES));
220 [js_injection_client_ didReceiveResponse:test_response];
221 [js_injection_client_ didLoadData:test_data_];
222 [js_injection_client_ didFinishLoading];
224 EXPECT_EQ(1u, GetScriptTagCount());
225 ExpectConsistentContentLength();
228 // Tests that injection occurs just following the html tag when there is < 1KB
229 // of padding preceding the html start tag.
230 TEST_F(CRWJSInjectionNetworkClientTest, LessThan1KBBeforeHTMLTag) {
231 base::scoped_nsobject<NSString> test_string(
232 [[NSString alloc] initWithData:test_data_
233 encoding:NSUTF8StringEncoding]);
234 NSData* padded_data = [[GetLongString(900u)
235 stringByAppendingString:test_string]
236 dataUsingEncoding:NSUTF8StringEncoding];
237 test_data_.reset([padded_data retain]);
238 ASSERT_TRUE(test_data_);
240 base::scoped_nsobject<CRNHTTPURLResponse> test_response(
241 CreateTestResponse(YES));
242 [js_injection_client_ didReceiveResponse:test_response];
243 [js_injection_client_ didLoadData:test_data_];
244 [js_injection_client_ didFinishLoading];
246 EXPECT_EQ(1u, GetScriptTagCount());
247 ExpectConsistentContentLength();
250 TEST_F(CRWJSInjectionNetworkClientTest, PaddedMissingHTMLTag) {
251 base::scoped_nsobject<NSString> test_string(
252 [[NSString alloc] initWithData:test_data_
253 encoding:NSUTF8StringEncoding]);
255 [[test_string stringByReplacingOccurrencesOfString:@"<html>"
256 withString:@""] retain]);
257 NSData* padded_data = [[GetLongString(900u)
258 stringByAppendingString:test_string]
259 dataUsingEncoding:NSUTF8StringEncoding];
260 test_data_.reset([padded_data retain]);
261 ASSERT_TRUE(test_data_);
263 base::scoped_nsobject<CRNHTTPURLResponse> test_response(
264 CreateTestResponse(YES));
265 [js_injection_client_ didReceiveResponse:test_response];
266 [js_injection_client_ didLoadData:test_data_];
267 [js_injection_client_ didFinishLoading];
269 EXPECT_EQ(1u, GetScriptTagCount());
270 ExpectConsistentContentLength();
273 // Tests scenario in which data is loaded one byte at a time, as might occur
274 // under a slow connection.
275 TEST_F(CRWJSInjectionNetworkClientTest, FragmentedDataLoad) {
276 base::scoped_nsobject<CRNHTTPURLResponse> test_response(
277 CreateTestResponse(YES));
278 [js_injection_client_ didReceiveResponse:test_response];
279 // Load data one byte at a time.
280 for (NSUInteger i = 0; i < [test_data_ length]; i++) {
281 [js_injection_client_ didLoadData:
282 [test_data_ subdataWithRange:NSMakeRange(i, 1u)]];
284 [js_injection_client_ didFinishLoading];
286 EXPECT_EQ(1u, GetScriptTagCount());
287 ExpectConsistentContentLength();