chromium/ios/chrome/browser/variations/model/ios_chrome_variations_seed_fetcher_unittest.mm

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/variations/model/ios_chrome_variations_seed_fetcher.h"

#import "base/run_loop.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/time/time.h"
#import "build/branding_buildflags.h"
#import "components/variations/seed_response.h"
#import "components/variations/variations_switches.h"
#import "components/variations/variations_url_constants.h"
#import "components/version_info/version_info.h"
#import "ios/chrome/browser/variations/model/constants.h"
#import "ios/chrome/browser/variations/model/ios_chrome_variations_seed_store.h"
#import "net/http/http_status_code.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"

// The following headers should be imported after their non-testing
// counterparts.
#import "ios/chrome/browser/variations/model/ios_chrome_variations_seed_fetcher+testing.h"
#import "ios/chrome/browser/variations/model/ios_chrome_variations_seed_store+testing.h"

namespace {

using variations::kDefaultServerUrl;
using variations::switches::kFakeVariationsChannel;
using variations::switches::kVariationsServerURL;

// Histogram names for seed fetch time and result.
const char kSeedFetchResultHistogram[] =
    "IOS.Variations.FirstRun.SeedFetchResult";
const char kSeedFetchTimeHistogram[] = "IOS.Variations.FirstRun.SeedFetchTime";

// Fake server url.
const NSString* test_server = @"https://test.finch.server";

// Type definition of a block that retrieves the value of an HTTP header from a
// custom NSDictionary instead of actual NSURLHTTPRequest header.
typedef void (^MockValueForHTTPHeaderField)(NSInvocation*);

// Retrieve the block that could substitute for -[NSHTTPURLResponse
// valueForHTTPHeaderField:].
MockValueForHTTPHeaderField GetMockMethodWithHeader(
    NSDictionary<NSString*, NSString*>* headers) {
  void (^output_block)(NSInvocation*) = ^(NSInvocation* invocation) {
    __weak NSString* arg;
    [invocation getArgument:&arg atIndex:2];
    __weak NSString* value = headers[arg];
    [invocation setReturnValue:&value];
  };
  return output_block;
}

}  // namespace

#pragma mark - Tests

// Tests the IOSChromeVariationsSeedFetcher.
class IOSChromeVariationsSeedFetcherTest : public PlatformTest {
 protected:
  void TearDown() override {
    [IOSChromeVariationsSeedStore resetForTesting];
    [IOSChromeVariationsSeedFetcher resetFetchingStatusForTesting];
    PlatformTest::TearDown();
  }
};

// Tests that the request to the finch server would not be made when seed
// fetching is not enabled.
//
// Note: this would happen only when build is NOT Google Chrome branded.
#if !BUILDFLAG(GOOGLE_CHROME_BRANDING)
TEST_F(IOSChromeVariationsSeedFetcherTest,
       testThatRequestIsNotMadeWhenFetchSeedNotEnabled) {
  // Attach mock delegate.
  id delegate =
      OCMProtocolMock(@protocol(IOSChromeVariationsSeedFetcherDelegate));
  OCMExpect([delegate variationsSeedFetcherDidCompleteFetchWithSuccess:NO]);
  // Start fetching seed from fetcher. Seed fetching is disabled by default in
  // tests.
  base::HistogramTester histogram_tester;
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[]];
  fetcher.delegate = delegate;
  [fetcher startSeedFetch];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.05));
  EXPECT_OCMOCK_VERIFY(delegate);
  histogram_tester.ExpectTotalCount(kSeedFetchTimeHistogram, 0);
  histogram_tester.ExpectTotalCount(kSeedFetchResultHistogram, 0);
}
#endif  // !BUILDFLAG(GOOGLE_CHROME_BRANDING)

// Tests that the request to the finch server would be made when seed fetching
// is enabled.
TEST_F(IOSChromeVariationsSeedFetcherTest,
       testThatRequestIsMadeWhenFetchSeedEnabled) {
  // Instantiate mocks.
  BOOL (^request_matcher)(NSMutableURLRequest* request) =
      ^BOOL(NSMutableURLRequest* request) {
        // The NSString method `hasPrefix` does not take parameter of type
        // 'const NSString *__strong`.
        NSString* prefix = [NSString stringWithFormat:@"%@", test_server];
        return [request.URL.absoluteString hasPrefix:prefix] &&
               [request.allHTTPHeaderFields[@"A-IM"] isEqualToString:@"gzip"];
      };
  id mock_url_session = OCMClassMock([NSURLSession class]);
  OCMStub([mock_url_session sharedSession]).andReturn(mock_url_session);
  OCMExpect([mock_url_session
      dataTaskWithRequest:[OCMArg checkWithBlock:request_matcher]
        completionHandler:[OCMArg any]]);
  // Start fetching seed from fetcher. Pass a fake value for
  // `kVariationsServerURL` to enable testing.
  NSString* argument =
      [NSString stringWithFormat:@"--%@=%@",
                                 base::SysUTF8ToNSString(kVariationsServerURL),
                                 test_server];
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[ argument ]];
  [fetcher startSeedFetch];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.05));
  EXPECT_OCMOCK_VERIFY(mock_url_session);
}

// Tests that the default variations url is correct.
TEST_F(IOSChromeVariationsSeedFetcherTest, TestDefaultVariationsURL) {
  // Instantiate mocks and expectations.
  BOOL (^request_matcher)(NSMutableURLRequest* request) =
      ^BOOL(NSMutableURLRequest* request) {
        NSString* expected_url_prefix = [NSString
            stringWithFormat:@"%@?osname=ios&milestone=",
                             base::SysUTF8ToNSString(kDefaultServerUrl)];
        return [request.URL.absoluteString hasPrefix:expected_url_prefix];
      };
  id mock_url_session = OCMClassMock([NSURLSession class]);
  OCMStub([mock_url_session sharedSession]).andReturn(mock_url_session);
  OCMExpect([mock_url_session
      dataTaskWithRequest:[OCMArg checkWithBlock:request_matcher]
        completionHandler:[OCMArg any]]);
  // Fetch and check.
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[]];
  [fetcher doActualFetch];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.05));
  EXPECT_OCMOCK_VERIFY(mock_url_session);
}

// Tests that the variations url is correct with a fake channel.
TEST_F(IOSChromeVariationsSeedFetcherTest, TestVariationsURLWithFakeChannel) {
  NSString* test_channel = @"fake_channel";
  // Instantiate mocks.
  BOOL (^request_matcher)(NSMutableURLRequest* request) =
      ^BOOL(NSMutableURLRequest* request) {
        NSString* expected_url = [NSString
            stringWithFormat:@"%@?osname=ios&milestone=%@&channel=%@",
                             base::SysUTF8ToNSString(kDefaultServerUrl),
                             base::SysUTF8ToNSString(
                                 version_info::GetMajorVersionNumber()),
                             test_channel];
        return [request.URL.absoluteString isEqualToString:expected_url];
      };
  id mock_url_session = OCMClassMock([NSURLSession class]);
  OCMStub([mock_url_session sharedSession]).andReturn(mock_url_session);
  OCMExpect([mock_url_session
      dataTaskWithRequest:[OCMArg checkWithBlock:request_matcher]
        completionHandler:[OCMArg any]]);

  // Pass a fake value for `kFakeVariationsChannel` to make sure it's
  // represented in the URL.
  NSString* test_channel_argument = [NSString
      stringWithFormat:@"--%@=%@",
                       base::SysUTF8ToNSString(kFakeVariationsChannel),
                       test_channel];
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc]
          initWithArguments:@[ test_channel_argument ]];
  [fetcher doActualFetch];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.05));
  EXPECT_OCMOCK_VERIFY(mock_url_session);
}

// Tests that the variations url is correct with a fake channel and fake
// server.
TEST_F(IOSChromeVariationsSeedFetcherTest,
       TestVariationsURLWithFakeChannelAndFakeServer) {
  NSString* test_channel = @"fake_channel";
  // Instantiate mocks.
  BOOL (^request_matcher)(NSMutableURLRequest* request) = ^BOOL(
      NSMutableURLRequest* request) {
    NSString* expected_url = [NSString
        stringWithFormat:@"%@?osname=ios&milestone=%@&channel=%@", test_server,
                         base::SysUTF8ToNSString(
                             version_info::GetMajorVersionNumber()),
                         test_channel];
    return [request.URL.absoluteString isEqualToString:expected_url];
  };
  id mock_url_session = OCMClassMock([NSURLSession class]);
  OCMStub([mock_url_session sharedSession]).andReturn(mock_url_session);
  OCMExpect([mock_url_session
      dataTaskWithRequest:[OCMArg checkWithBlock:request_matcher]
        completionHandler:[OCMArg any]]);

  // Pass a fake value for `kFakeVariationsChannel` to make sure it's
  // represented in the URL.
  NSString* test_channel_argument = [NSString
      stringWithFormat:@"--%@=%@",
                       base::SysUTF8ToNSString(kFakeVariationsChannel),
                       test_channel];
  // Pass a fake value for `kVariationsServerURL` to enable testing.
  NSString* test_server_argument =
      [NSString stringWithFormat:@"--%@=%@",
                                 base::SysUTF8ToNSString(kVariationsServerURL),
                                 test_server];

  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc]
          initWithArguments:@[ test_channel_argument, test_server_argument ]];
  [fetcher doActualFetch];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.05));
  EXPECT_OCMOCK_VERIFY(mock_url_session);
}

// Tests that the see won't be created when an HTTP timeout error occurs, and
// that the delegate would be notified so.
TEST_F(IOSChromeVariationsSeedFetcherTest, testHTTPTimeoutError) {
  base::HistogramTester histogram_tester;
  // Instantiate the mocks
  id timeout_error = OCMClassMock([NSError class]);
  OCMStub([timeout_error code]).andReturn(NSURLErrorTimedOut);
  id response_ok = OCMClassMock([NSHTTPURLResponse class]);
  OCMStub([response_ok statusCode]).andReturn(net::HTTP_OK);
  id delegate =
      OCMProtocolMock(@protocol(IOSChromeVariationsSeedFetcherDelegate));
  OCMExpect([delegate variationsSeedFetcherDidCompleteFetchWithSuccess:NO]);
  // Invalidate the NSURLSession so the original request completion handler
  // would not be executed.
  id mock_url_session = OCMClassMock([NSURLSession class]);
  OCMStub([mock_url_session sharedSession]).andReturn(mock_url_session);
  // Start the test.
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[]];
  fetcher.delegate = delegate;
  [fetcher doActualFetch];
  [fetcher seedRequestDidCompleteWithData:nil
                                 response:response_ok
                                    error:timeout_error];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.05));
  EXPECT_EQ([IOSChromeVariationsSeedStore popSeed], nil);
  histogram_tester.ExpectTotalCount(kSeedFetchTimeHistogram, 0);
  histogram_tester.ExpectUniqueSample(
      kSeedFetchResultHistogram, IOSSeedFetchException::kHTTPSRequestTimeout,
      1);
}

// Tests that the see won't be created when the response code is unexpected, and
// that the delegate would be notified so.
TEST_F(IOSChromeVariationsSeedFetcherTest, testHTTPResponseCodeError) {
  base::HistogramTester histogram_tester;
  // Instantiate mocks.
  id response_error = OCMClassMock([NSHTTPURLResponse class]);
  OCMStub([response_error statusCode]).andReturn(net::HTTP_NOT_FOUND);
  id delegate =
      OCMProtocolMock(@protocol(IOSChromeVariationsSeedFetcherDelegate));
  OCMExpect([delegate variationsSeedFetcherDidCompleteFetchWithSuccess:NO]);
  // Invalidate the NSURLSession so the original request completion handler
  // would not be executed.
  id mock_url_session = OCMClassMock([NSURLSession class]);
  OCMStub([mock_url_session sharedSession]).andReturn(mock_url_session);
  // Start the test.
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[]];
  fetcher.delegate = delegate;
  [fetcher doActualFetch];
  [fetcher seedRequestDidCompleteWithData:nil
                                 response:response_error
                                    error:nil];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.05));
  EXPECT_EQ([IOSChromeVariationsSeedStore popSeed], nil);
  EXPECT_OCMOCK_VERIFY(delegate);
  histogram_tester.ExpectTotalCount(kSeedFetchTimeHistogram, 0);
  histogram_tester.ExpectUniqueSample(kSeedFetchResultHistogram,
                                      net::HTTP_NOT_FOUND, 1);
}

// Tests that the seed creation would be attempted when there is a request
// with the HTTP response, and that the delegate would be notified if it
// fails.
TEST_F(IOSChromeVariationsSeedFetcherTest,
       testValidHTTPResponseWithFailingSeedCreation) {
  base::HistogramTester histogram_tester;
  // Setup.
  id delegate =
      OCMProtocolMock(@protocol(IOSChromeVariationsSeedFetcherDelegate));
  OCMExpect([delegate variationsSeedFetcherDidCompleteFetchWithSuccess:NO]);
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[]];
  fetcher.delegate = delegate;
  id response = OCMClassMock([NSHTTPURLResponse class]);
  OCMStub([response statusCode]).andReturn(net::HTTP_OK);
  // Invalidate the NSURLSession so the original request completion handler
  // would not be executed.
  id mock_url_session = OCMClassMock([NSURLSession class]);
  OCMStub([mock_url_session sharedSession]).andReturn(mock_url_session);
  // Execute test.
  [fetcher doActualFetch];
  [fetcher seedRequestDidCompleteWithData:nil response:response error:nil];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.05));
  EXPECT_EQ([IOSChromeVariationsSeedStore popSeed], nil);
  EXPECT_OCMOCK_VERIFY(delegate);
  histogram_tester.ExpectTotalCount(kSeedFetchTimeHistogram, 1);
  histogram_tester.ExpectUniqueSample(
      kSeedFetchResultHistogram, IOSSeedFetchException::kInvalidIMHeader, 1);
}

// Tests that the seed would not be created when the instance manipulation
// header does not exist.
TEST_F(IOSChromeVariationsSeedFetcherTest, testNoIMHeader) {
  // Set up.
  NSDictionary<NSString*, NSString*>* header = @{
    @"X-Seed-Signature" : [NSDate now].description,
    @"X-Country" : @"US",
  };
  id response = OCMClassMock([NSHTTPURLResponse class]);
  OCMStub([response valueForHTTPHeaderField:[OCMArg any]])
      .andDo(GetMockMethodWithHeader(header));
  // Execute test.
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[]];
  std::unique_ptr<variations::SeedResponse> seed =
      [fetcher seedResponseForHTTPResponse:response data:nil];
  EXPECT_EQ(seed, nullptr);
}

// Tests that the seed would not be created when the instance manipulation
// header is not "gzip".
TEST_F(IOSChromeVariationsSeedFetcherTest, testBadIMHeader) {
  // Set up.
  NSDictionary<NSString*, NSString*>* header = @{
    @"X-Seed-Signature" : [NSDate now].description,
    @"X-Country" : @"US",
    @"IM" : @"not_gzip"
  };
  id response = OCMClassMock([NSHTTPURLResponse class]);
  OCMStub([response valueForHTTPHeaderField:[OCMArg any]])
      .andDo(GetMockMethodWithHeader(header));
  // Execute test.
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[]];
  std::unique_ptr<variations::SeedResponse> seed =
      [fetcher seedResponseForHTTPResponse:response data:nil];
  EXPECT_EQ(seed, nullptr);
}

// Tests that the seed would not be created when the instance manipulation
// header has more than one value.
TEST_F(IOSChromeVariationsSeedFetcherTest, tesMoreThanOneIMHeaders) {
  // Set up.
  NSDictionary<NSString*, NSString*>* header = @{
    @"X-Seed-Signature" : [NSDate now].description,
    @"X-Country" : @"US",
    @"IM" : @"gzip,somethingelse"
  };
  id response = OCMClassMock([NSHTTPURLResponse class]);
  OCMStub([response valueForHTTPHeaderField:[OCMArg any]])
      .andDo(GetMockMethodWithHeader(header));
  // Execute test.
  IOSChromeVariationsSeedFetcher* fetcher =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[]];
  std::unique_ptr<variations::SeedResponse> seed =
      [fetcher seedResponseForHTTPResponse:response data:nil];
  EXPECT_EQ(seed, nullptr);
}

// Tests that the seed would be created with property values extracted from
// the HTTP response with expected header format, and that the delegate would
// be notified of the successful seed creation.
TEST_F(IOSChromeVariationsSeedFetcherTest,
       testValidHTTPResponseWithSuccessfulSeedCreation) {
  base::HistogramTester histogram_tester;
  NSString* signature = [NSDate now].description;
  NSString* country = @"US";
  NSDictionary<NSString*, NSString*>* headers = @{
    @"X-Seed-Signature" : signature,
    @"X-Country" : country,
    @"IM" : @" gzip"
  };
  id response = OCMClassMock([NSHTTPURLResponse class]);
  OCMStub([response statusCode]).andReturn(net::HTTP_OK);
  OCMStub([response valueForHTTPHeaderField:[OCMArg any]])
      .andDo(GetMockMethodWithHeader(headers));
  // Setup.
  id delegate =
      OCMProtocolMock(@protocol(IOSChromeVariationsSeedFetcherDelegate));
  OCMExpect([delegate variationsSeedFetcherDidCompleteFetchWithSuccess:YES]);
  IOSChromeVariationsSeedFetcher* fetcher_with_seed =
      [[IOSChromeVariationsSeedFetcher alloc] initWithArguments:@[]];
  fetcher_with_seed.delegate = delegate;
  // Invalidate the NSURLSession so the original request completion handler
  // would not be executed.
  id mock_url_session = OCMClassMock([NSURLSession class]);
  OCMStub([mock_url_session sharedSession]).andReturn(mock_url_session);
  // Execute test.
  [fetcher_with_seed doActualFetch];
  [fetcher_with_seed seedRequestDidCompleteWithData:nil
                                           response:response
                                              error:nil];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.05));
  auto seed = [IOSChromeVariationsSeedStore popSeed];
  ASSERT_NE(seed, nullptr);
  EXPECT_EQ(seed->signature, base::SysNSStringToUTF8(signature));
  EXPECT_EQ(seed->country, base::SysNSStringToUTF8(country));
  EXPECT_EQ(seed->data, "");
  EXPECT_OCMOCK_VERIFY(delegate);
  histogram_tester.ExpectTotalCount(kSeedFetchTimeHistogram, 1);
  histogram_tester.ExpectUniqueSample(kSeedFetchResultHistogram, net::HTTP_OK,
                                      1);
}