chromium/components/open_from_clipboard/clipboard_recent_content_ios_unittest.mm

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

#include "components/open_from_clipboard/clipboard_recent_content_ios.h"

#import <CoreGraphics/CoreGraphics.h>
#import <UIKit/UIKit.h>

#include <memory>

#include "base/functional/bind.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "base/test/task_environment.h"
#import "components/open_from_clipboard/clipboard_recent_content_impl_ios.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/platform_test.h"

using base::test::ios::WaitUntilConditionOrTimeout;
using base::test::ios::kWaitForCookiesTimeout;
using base::test::ios::kWaitForActionTimeout;

namespace {

UIImage* TestUIImage(UIColor* color = [UIColor redColor]) {
  CGRect frame = CGRectMake(0, 0, 1.0, 1.0);
  UIGraphicsBeginImageContext(frame.size);

  CGContextRef context = UIGraphicsGetCurrentContext();
  CGContextSetFillColorWithColor(context, color.CGColor);
  CGContextFillRect(context, frame);

  UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();

  return image;
}

void SetPasteboardImage(UIImage* image) {
  [[UIPasteboard generalPasteboard] setImage:image];
}

void SetPasteboardContent(const char* data) {
  [[UIPasteboard generalPasteboard]
               setValue:[NSString stringWithUTF8String:data]
      forPasteboardType:@"public.plain-text"];
}
const char kUnrecognizedURL[] = "bad://foo/";
const char kRecognizedURL[] = "good://bar/";
const char kRecognizedURL2[] = "good://bar/2";
const char kAppSpecificURL[] = "test://qux/";
const char kAppSpecificScheme[] = "test";
const char kRecognizedScheme[] = "good";
NSTimeInterval kLongerThanMaxAge = 60 * 60 * 7;
NSTimeInterval kMaxAge = 60 * 60 * 1;
}  // namespace

@interface ClipboardRecentContentImplIOSWithFakeUptime
    : ClipboardRecentContentImplIOS
@property(nonatomic) NSTimeInterval fakeUptime;

- (instancetype)initWithMaxAge:(NSTimeInterval)maxAge
             authorizedSchemes:(NSArray*)authorizedSchemes
                  userDefaults:(NSUserDefaults*)groupUserDefaults
         onlyUseClipboardAsync:(BOOL)onlyUseClipboardAsync
                        uptime:(NSTimeInterval)uptime;

@end

@implementation ClipboardRecentContentImplIOSWithFakeUptime

@synthesize fakeUptime = _fakeUptime;

- (instancetype)initWithMaxAge:(NSTimeInterval)maxAge
             authorizedSchemes:(NSSet*)authorizedSchemes
                  userDefaults:(NSUserDefaults*)groupUserDefaults
         onlyUseClipboardAsync:(BOOL)onlyUseClipboardAsync
                        uptime:(NSTimeInterval)uptime {
  self = [super initWithMaxAge:maxAge
             authorizedSchemes:authorizedSchemes
                  userDefaults:groupUserDefaults
         onlyUseClipboardAsync:onlyUseClipboardAsync
                      delegate:nil];
  if (self) {
    _fakeUptime = uptime;
  }
  return self;
}

- (NSTimeInterval)uptime {
  return self.fakeUptime;
}

@end

class ClipboardRecentContentIOSWithFakeUptime
    : public ClipboardRecentContentIOS {
 public:
  ClipboardRecentContentIOSWithFakeUptime(
      ClipboardRecentContentImplIOS* implementation)
      : ClipboardRecentContentIOS(implementation) {}
};

class ClipboardRecentContentIOSTest : public ::testing::Test {
 protected:
  ClipboardRecentContentIOSTest() {
    // By default, set that the device booted 10 days ago.
    ResetClipboardRecentContent(kAppSpecificScheme, base::Days(10));
  }

  void SimulateDeviceRestart() {
    ResetClipboardRecentContent(kAppSpecificScheme, base::Seconds(0));
  }

  void ResetClipboardRecentContent(const std::string& application_scheme,
                                   base::TimeDelta time_delta) {
    ClipboardRecentContentImplIOSWithFakeUptime*
        clipboard_content_implementation =
            [[ClipboardRecentContentImplIOSWithFakeUptime alloc]
                       initWithMaxAge:kMaxAge
                    authorizedSchemes:@[
                      base::SysUTF8ToNSString(kRecognizedScheme),
                      base::SysUTF8ToNSString(application_scheme)
                    ]
                         userDefaults:[NSUserDefaults standardUserDefaults]
                onlyUseClipboardAsync:NO
                               uptime:time_delta.InSecondsF()];

    clipboard_content_ =
        std::make_unique<ClipboardRecentContentIOSWithFakeUptime>(
            clipboard_content_implementation);

    // Keep a weak pointer to the ClipboardRecentContentImplIOS to allow
    // updating the fake pasteboard change date.
    clipboard_content_implementation_ = clipboard_content_implementation;
  }

  void SetStoredPasteboardChangeDate(NSDate* change_date) {
    clipboard_content_implementation_.lastPasteboardChangeDate = change_date;
    [clipboard_content_implementation_ saveToUserDefaults];
  }

 protected:
  std::unique_ptr<ClipboardRecentContentIOSWithFakeUptime> clipboard_content_;
  __weak ClipboardRecentContentImplIOSWithFakeUptime*
      clipboard_content_implementation_;

  base::test::TaskEnvironment task_environment_;

  void VerifyClipboardTypeExists(ClipboardContentType type, bool exists) {
    __block BOOL callback_called = NO;
    __block BOOL type_exists = NO;
    std::set<ClipboardContentType> types;
    types.insert(type);
    clipboard_content_->HasRecentContentFromClipboard(
        types, base::BindOnce(^(std::set<ClipboardContentType> found_types) {
          callback_called = YES;
          type_exists = found_types.find(type) != found_types.end();
        }));

    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForCookiesTimeout, ^bool {
      base::RunLoop().RunUntilIdle();
      return callback_called;
    }));
    EXPECT_EQ(exists, type_exists);
  }

  void VerifyClipboardURLExists(const char* expected_url) {
    VerifyClipboardTypeExists(ClipboardContentType::URL, true);

    __block BOOL callback_called = NO;
    __block std::optional<GURL> optional_gurl;
    clipboard_content_->GetRecentURLFromClipboard(
        base::BindOnce(^(std::optional<GURL> copied_url) {
          optional_gurl = copied_url;
          callback_called = YES;
        }));
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForCookiesTimeout, ^bool {
      base::RunLoop().RunUntilIdle();
      return callback_called;
    }));
    ASSERT_TRUE(optional_gurl.has_value());
    EXPECT_STREQ(expected_url, optional_gurl.value().spec().c_str());
  }

  bool VerifyCacheClipboardContentTypeExists(ClipboardContentType type) {
    std::optional<std::set<ClipboardContentType>> cached_content_types =
        clipboard_content_->GetCachedClipboardContentTypes();
    if (cached_content_types.has_value()) {
      return cached_content_types.value().find(type) !=
             cached_content_types.value().end();
    } else {
      return false;
    }
  }

  void VerifiyClipboardURLIsInvalid() {
    VerifyClipboardTypeExists(ClipboardContentType::URL, true);

    __block BOOL callback_called = NO;
    __block std::optional<GURL> optional_gurl;
    clipboard_content_->GetRecentURLFromClipboard(
        base::BindOnce(^(std::optional<GURL> copied_url) {
          optional_gurl = copied_url;
          callback_called = YES;
        }));
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForCookiesTimeout, ^bool {
      base::RunLoop().RunUntilIdle();
      return callback_called;
    }));
    EXPECT_FALSE(optional_gurl.has_value());
  }

  bool WaitForClipboardContentTypesRefresh() {
    bool success = WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool() {
      return clipboard_content_->GetCachedClipboardContentTypes().has_value();
    });

    return success;
  }
};

TEST_F(ClipboardRecentContentIOSTest, SchemeFiltering) {
  // Test unrecognized URL.
  SetPasteboardContent(kUnrecognizedURL);
  VerifiyClipboardURLIsInvalid();

  // Test recognized URL.
  SetPasteboardContent(kRecognizedURL);
  VerifyClipboardURLExists(kRecognizedURL);

  // Test URL with app specific scheme.
  SetPasteboardContent(kAppSpecificURL);
  VerifyClipboardURLExists(kAppSpecificURL);

  // Test URL without app specific scheme.
  ResetClipboardRecentContent(std::string(), base::Days(10));

  SetPasteboardContent(kAppSpecificURL);
  VerifiyClipboardURLIsInvalid();
}

TEST_F(ClipboardRecentContentIOSTest, PasteboardURLObsolescence) {
  SetPasteboardContent(kRecognizedURL);

  // Test that recent pasteboard data is provided.
  VerifyClipboardURLExists(kRecognizedURL);

  // Test that old pasteboard data is not provided.
  SetStoredPasteboardChangeDate(
      [NSDate dateWithTimeIntervalSinceNow:-kLongerThanMaxAge]);

  VerifyClipboardTypeExists(ClipboardContentType::URL, false);
  VerifyClipboardTypeExists(ClipboardContentType::Text, false);

  // Tests that if chrome is relaunched, old pasteboard data is still
  // not provided.
  ResetClipboardRecentContent(kAppSpecificScheme, base::Days(10));
  VerifyClipboardTypeExists(ClipboardContentType::URL, false);
  VerifyClipboardTypeExists(ClipboardContentType::Text, false);

  SimulateDeviceRestart();
  // Tests that if the device is restarted, old pasteboard data is still
  // not provided.
  VerifyClipboardTypeExists(ClipboardContentType::URL, false);
  VerifyClipboardTypeExists(ClipboardContentType::Text, false);
}

TEST_F(ClipboardRecentContentIOSTest,
       CacheClipboardContentTypesUpdatesForCopiedURL) {
  SetPasteboardContent(kRecognizedURL);
  ASSERT_TRUE(WaitForClipboardContentTypesRefresh());

  EXPECT_TRUE(VerifyCacheClipboardContentTypeExists(ClipboardContentType::URL));
  EXPECT_FALSE(
      VerifyCacheClipboardContentTypeExists(ClipboardContentType::Image));
  EXPECT_FALSE(
      VerifyCacheClipboardContentTypeExists(ClipboardContentType::Text));
}

TEST_F(ClipboardRecentContentIOSTest,
       CacheClipboardContentTypesUpdatesForCopiedImage) {
  SetPasteboardImage(TestUIImage());
  ASSERT_TRUE(WaitForClipboardContentTypesRefresh());

  EXPECT_TRUE(
      VerifyCacheClipboardContentTypeExists(ClipboardContentType::Image));
  EXPECT_FALSE(
      VerifyCacheClipboardContentTypeExists(ClipboardContentType::URL));
  EXPECT_FALSE(
      VerifyCacheClipboardContentTypeExists(ClipboardContentType::Text));
}

TEST_F(ClipboardRecentContentIOSTest,
       CacheClipboardContentTypesUpdatesForCopiedText) {
  SetPasteboardContent("foobar");
  ASSERT_TRUE(WaitForClipboardContentTypesRefresh());

  EXPECT_TRUE(
      VerifyCacheClipboardContentTypeExists(ClipboardContentType::Text));
  EXPECT_FALSE(
      VerifyCacheClipboardContentTypeExists(ClipboardContentType::Image));
  EXPECT_FALSE(
      VerifyCacheClipboardContentTypeExists(ClipboardContentType::URL));
}

// Checks that if the pasteboard is marked as having confidential data, it is
// not returned.
TEST_F(ClipboardRecentContentIOSTest, ConfidentialPasteboardText) {
  [[UIPasteboard generalPasteboard]
      setItems:@[ @{
        @"public.plain-text" : @"hunter2",
        @"org.nspasteboard.ConcealedType" : @"hunter2"
      } ]
       options:@{}];

  VerifyClipboardTypeExists(ClipboardContentType::Text, false);
}

// Checks that if the user suppresses content, no text will be returned,
// and if the text changes, the new text will be returned again.
TEST_F(ClipboardRecentContentIOSTest, SuppressedPasteboardContent) {
  SetPasteboardContent(kRecognizedURL);

  // Test that recent pasteboard data is provided.
  VerifyClipboardURLExists(kRecognizedURL);

  // Suppress the content of the pasteboard.
  clipboard_content_->SuppressClipboardContent();

  // Check that the pasteboard content is suppressed.
  VerifyClipboardTypeExists(ClipboardContentType::URL, false);

  // Create a new clipboard content to test persistence.
  ResetClipboardRecentContent(kAppSpecificScheme, base::Days(10));

  // Check that the pasteboard content is still suppressed.
  VerifyClipboardTypeExists(ClipboardContentType::URL, false);

  // Check that even if the device is restarted, pasteboard content is
  // still suppressed.
  SimulateDeviceRestart();
  VerifyClipboardTypeExists(ClipboardContentType::URL, false);

  // Check that if the pasteboard changes, the new content is not
  // suppressed anymore.
  SetPasteboardContent(kRecognizedURL2);
  VerifyClipboardURLExists(kRecognizedURL2);
}

// TODO(crbug.com/40275048): This test is flaky.
// Checks that if the user suppresses content, no image will be returned,
// and if the image changes, the new image will be returned again.
TEST_F(ClipboardRecentContentIOSTest, DISABLED_SuppressedPasteboardImage) {
  SetPasteboardImage(TestUIImage());

  // Test that recent pasteboard data is provided.
  VerifyClipboardTypeExists(ClipboardContentType::Image, true);

  // Suppress the content of the pasteboard.
  clipboard_content_->SuppressClipboardContent();

  // Check that the pasteboard content is suppressed.
  VerifyClipboardTypeExists(ClipboardContentType::Image, false);

  // Create a new clipboard content to test persistence.
  ResetClipboardRecentContent(kAppSpecificScheme, base::Days(10));

  // Check that the pasteboard content is still suppressed.
  VerifyClipboardTypeExists(ClipboardContentType::Image, false);

  // Check that even if the device is restarted, pasteboard content is
  // still suppressed.
  SimulateDeviceRestart();
  VerifyClipboardTypeExists(ClipboardContentType::Image, false);

  // Check that if the pasteboard changes, the new content is not
  // suppressed anymore.
  SetPasteboardImage(TestUIImage([UIColor greenColor]));
  VerifyClipboardTypeExists(ClipboardContentType::Image, true);
}

// TODO(crbug.com/40275048): This test is flaky.
// Checks that if user copies something other than a string we don't cache the
// string in pasteboard.
TEST_F(ClipboardRecentContentIOSTest,
       DISABLED_AddingNonStringRemovesCachedString) {
  SetPasteboardContent(kRecognizedURL);

  // Test that recent pasteboard data is provided as url.
  VerifyClipboardURLExists(kRecognizedURL);
  VerifyClipboardTypeExists(ClipboardContentType::Text, false);
  // Image pasteboard should be empty.
  VerifyClipboardTypeExists(ClipboardContentType::Image, false);

  // Overwrite pasteboard with an image.
  SetPasteboardImage(TestUIImage());

  // Url and text pasteboard should appear empty.
  VerifyClipboardTypeExists(ClipboardContentType::URL, false);
  VerifyClipboardTypeExists(ClipboardContentType::Text, false);
  // Image pasteboard should be full
  VerifyClipboardTypeExists(ClipboardContentType::Image, true);

  // Tests that if URL is added again, pasteboard provides it normally.
  SetPasteboardContent(kRecognizedURL);
  VerifyClipboardURLExists(kRecognizedURL);
  VerifyClipboardTypeExists(ClipboardContentType::Text, false);
  // Image pasteboard should be empty.
  VerifyClipboardTypeExists(ClipboardContentType::Image, false);
}