blob: b5353d5dc850920a0bff2287dfabf9143cf81972 [file] [log] [blame]
// Copyright 2014 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 "ios/web/net/clients/crw_js_injection_network_client.h"
#import <Foundation/Foundation.h>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/path_service.h"
#include "base/strings/sys_string_conversions.h"
#import "ios/net/clients/crn_network_client_protocol.h"
#import "ios/net/crn_http_url_response.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
namespace {
// Returns an NSString filled with the char 'a' of length |length|.
NSString* GetLongString(NSUInteger length) {
base::scoped_nsobject<NSMutableData> data(
[[NSMutableData alloc] initWithLength:length]);
memset([data mutableBytes], 'a', length);
NSString* long_string =
[[NSString alloc] initWithData:data
encoding:NSASCIIStringEncoding];
return [long_string autorelease];
}
}
// Class to serve as underlying client for JS injection client to expose
// data and responses that are passed on from the JS injection client.
@interface UnderlyingClient : CRNForwardingNetworkClient {
base::scoped_nsobject<NSMutableData> _loadedData;
base::scoped_nsobject<NSURLResponse> _receivedResponse;
}
// Returns all data loaded by the client.
- (NSData*)loadedData;
// Returns response received by the client.
- (NSURLResponse*)receivedResponse;
@end
@implementation UnderlyingClient
- (instancetype)init {
if ((self = [super init])) {
_loadedData.reset([[NSMutableData alloc] init]);
}
return self;
}
- (NSData*)loadedData {
return _loadedData.get();
}
- (NSURLResponse*)receivedResponse {
return _receivedResponse.get();
}
- (void)didLoadData:(NSData*)data {
[_loadedData appendData:data];
[super didLoadData:data];
}
- (void)didReceiveResponse:(NSURLResponse*)response {
_receivedResponse.reset([response copy]);
[super didReceiveResponse:response];
}
@end
namespace {
const char kTestFile[] = "ios/web/test/data/chrome.html";
class CRWJSInjectionNetworkClientTest : public testing::Test {
public:
CRWJSInjectionNetworkClientTest() {}
void SetUp() override {
// Set up mock original network client proxy.
mock_web_proxy_.reset([[OCMockObject
niceMockForProtocol:@protocol(CRNNetworkClientProtocol)] retain]);
// Set up underlying client to inspect data and responses passed on by
// the JS injection client.
underlying_client_.reset([[UnderlyingClient alloc] init]);
[underlying_client_ setUnderlyingClient:
static_cast<id<CRNNetworkClientProtocol>>(mock_web_proxy_)];
// Link mock proxy into the JSInjectionNetworkClient.
js_injection_client_.reset([[CRWJSInjectionNetworkClient alloc] init]);
[js_injection_client_ setUnderlyingClient:underlying_client_];
// Load data for testing
base::FilePath file_path;
ASSERT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &file_path));
file_path = file_path.AppendASCII(kTestFile);
test_data_.reset([[NSData dataWithContentsOfFile:
base::SysUTF8ToNSString(file_path.value())] retain]);
ASSERT_TRUE(test_data_);
}
void TearDown() override { EXPECT_OCMOCK_VERIFY(mock_web_proxy_); }
protected:
// Returns a CRNHTTPURLResponse. If |include_content_length|, header includes
// Content-Length set to the length of test_data_.
CRNHTTPURLResponse* CreateTestResponse(BOOL include_content_length);
// Returns number of times an injected cr_web script tag is found in the
// underlying client's loaded data. Script tag should immediately follow
// the html start tag, if it exists, or should be injected before any header,
// if the first tag is something other than an html start tag.
NSUInteger GetScriptTagCount() const;
// Checks that if response forwarded to the underlying client has header field
// Content-Length, the value matches the length of the data.
void ExpectConsistentContentLength();
base::scoped_nsobject<CRWJSInjectionNetworkClient> js_injection_client_;
base::scoped_nsobject<UnderlyingClient> underlying_client_;
base::scoped_nsobject<OCMockObject> mock_web_proxy_;
base::scoped_nsobject<NSData> test_data_;
};
CRNHTTPURLResponse* CRWJSInjectionNetworkClientTest::CreateTestResponse(
BOOL include_content_length) {
NSMutableDictionary *headers = [NSMutableDictionary
dictionaryWithDictionary:@{ @"Content-Type" : @"text/html" }];
if (include_content_length) {
headers[@"Content-Length"] = @([test_data_ length]).stringValue;
}
return [[CRNHTTPURLResponse alloc]
initWithURL:[NSURL URLWithString:@"http://testjsinjection.html"]
statusCode:200
HTTPVersion:@"HTTP/1.1"
headerFields:headers];
};
NSUInteger CRWJSInjectionNetworkClientTest::GetScriptTagCount() const {
base::scoped_nsobject<NSString> data_string(
[[NSString alloc] initWithData:[underlying_client_ loadedData]
encoding:NSUTF8StringEncoding]);
NSRegularExpression* script_tag_reg_exp = [NSRegularExpression
regularExpressionWithPattern:@"(^|<html>)<script src="
"\"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-"
"[0-9a-f]{4}-[0-9a-f]{12}+_crweb\\.js\" "
"charset=\"utf-8\"></script>"
options:NSRegularExpressionCaseInsensitive
error:nil];
return [script_tag_reg_exp
numberOfMatchesInString:data_string
options:0
range:NSMakeRange(0, [data_string length])];
}
void CRWJSInjectionNetworkClientTest::ExpectConsistentContentLength() {
NSData* forwarded_data = [underlying_client_ loadedData];
NSDictionary* output_headers =
[static_cast<NSHTTPURLResponse*>([underlying_client_ receivedResponse])
allHeaderFields];
ASSERT_TRUE(output_headers);
NSInteger content_length = [output_headers[@"Content-Length"] integerValue];
if (content_length) {
EXPECT_EQ(static_cast<NSInteger>([forwarded_data length]),
(content_length));
}
}
} // namespace
#pragma mark - Tests
// Tests injection where response header has Content-Length. Checks that
// Content-Length is updated to match new size of data.
TEST_F(CRWJSInjectionNetworkClientTest, InjectionWithContentLength) {
base::scoped_nsobject<CRNHTTPURLResponse> test_response(
CreateTestResponse(YES));
[js_injection_client_ didReceiveResponse:test_response];
[js_injection_client_ didLoadData:test_data_];
[js_injection_client_ didFinishLoading];
EXPECT_EQ(1u, GetScriptTagCount());
ExpectConsistentContentLength();
}
// Tests injection where response header does not have Content-Length.
TEST_F(CRWJSInjectionNetworkClientTest, InjectionWithoutContentLength) {
base::scoped_nsobject<CRNHTTPURLResponse> test_response(
CreateTestResponse(NO));
[js_injection_client_ didReceiveResponse:test_response];
[js_injection_client_ didLoadData:test_data_];
[js_injection_client_ didFinishLoading];
EXPECT_EQ(1u, GetScriptTagCount());
ExpectConsistentContentLength();
}
// Tests that injection occurs at the beginning of the file if data has no html
// start tag but does have other tags within the first 1KB.
TEST_F(CRWJSInjectionNetworkClientTest, MissingHTMLTag) {
base::scoped_nsobject<NSString> test_string(
[[NSString alloc] initWithData:test_data_
encoding:NSUTF8StringEncoding]);
NSData* truncated_data =
[[test_string stringByReplacingOccurrencesOfString:@"<html>"
withString:@""]
dataUsingEncoding:NSUTF8StringEncoding];
test_data_.reset([truncated_data retain]);
ASSERT_TRUE(test_data_);
base::scoped_nsobject<CRNHTTPURLResponse> test_response(
CreateTestResponse(YES));
[js_injection_client_ didReceiveResponse:test_response];
[js_injection_client_ didLoadData:test_data_];
[js_injection_client_ didFinishLoading];
EXPECT_EQ(1u, GetScriptTagCount());
ExpectConsistentContentLength();
}
// Tests that injection occurs just following the html tag when there is < 1KB
// of padding preceding the html start tag.
TEST_F(CRWJSInjectionNetworkClientTest, LessThan1KBBeforeHTMLTag) {
base::scoped_nsobject<NSString> test_string(
[[NSString alloc] initWithData:test_data_
encoding:NSUTF8StringEncoding]);
NSData* padded_data = [[GetLongString(900u)
stringByAppendingString:test_string]
dataUsingEncoding:NSUTF8StringEncoding];
test_data_.reset([padded_data retain]);
ASSERT_TRUE(test_data_);
base::scoped_nsobject<CRNHTTPURLResponse> test_response(
CreateTestResponse(YES));
[js_injection_client_ didReceiveResponse:test_response];
[js_injection_client_ didLoadData:test_data_];
[js_injection_client_ didFinishLoading];
EXPECT_EQ(1u, GetScriptTagCount());
ExpectConsistentContentLength();
}
TEST_F(CRWJSInjectionNetworkClientTest, PaddedMissingHTMLTag) {
base::scoped_nsobject<NSString> test_string(
[[NSString alloc] initWithData:test_data_
encoding:NSUTF8StringEncoding]);
test_string.reset(
[[test_string stringByReplacingOccurrencesOfString:@"<html>"
withString:@""] retain]);
NSData* padded_data = [[GetLongString(900u)
stringByAppendingString:test_string]
dataUsingEncoding:NSUTF8StringEncoding];
test_data_.reset([padded_data retain]);
ASSERT_TRUE(test_data_);
base::scoped_nsobject<CRNHTTPURLResponse> test_response(
CreateTestResponse(YES));
[js_injection_client_ didReceiveResponse:test_response];
[js_injection_client_ didLoadData:test_data_];
[js_injection_client_ didFinishLoading];
EXPECT_EQ(1u, GetScriptTagCount());
ExpectConsistentContentLength();
}
// Tests scenario in which data is loaded one byte at a time, as might occur
// under a slow connection.
TEST_F(CRWJSInjectionNetworkClientTest, FragmentedDataLoad) {
base::scoped_nsobject<CRNHTTPURLResponse> test_response(
CreateTestResponse(YES));
[js_injection_client_ didReceiveResponse:test_response];
// Load data one byte at a time.
for (NSUInteger i = 0; i < [test_data_ length]; i++) {
[js_injection_client_ didLoadData:
[test_data_ subdataWithRange:NSMakeRange(i, 1u)]];
}
[js_injection_client_ didFinishLoading];
EXPECT_EQ(1u, GetScriptTagCount());
ExpectConsistentContentLength();
}