chromium/ios/chrome/browser/sessions/model/session_service_ios_unittest.mm

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

#import <Foundation/Foundation.h>

#import <memory>

#import "base/apple/foundation_util.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/files/scoped_temp_dir.h"
#import "base/path_service.h"
#import "base/run_loop.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/single_thread_task_runner.h"
#import "base/test/ios/wait_util.h"
#import "base/test/task_environment.h"
#import "ios/chrome/browser/sessions/model/session_service_ios.h"
#import "ios/chrome/browser/sessions/model/session_window_ios.h"
#import "ios/chrome/browser/sessions/model/session_window_ios_factory.h"
#import "ios/chrome/browser/shared/model/paths/paths.h"
#import "ios/chrome/browser/shared/model/web_state_list/test/fake_web_state_list_delegate.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/web/public/session/crw_session_storage.h"
#import "ios/web/public/session/serializable_user_data_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

namespace {

// The delay before saving.
constexpr base::TimeDelta kSaveDelay = base::Seconds(2.5);

// Fixture Class. Takes care of deleting the directory used to store test data.
class SessionServiceTest : public PlatformTest {
 public:
  SessionServiceTest()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
  }

  SessionServiceTest(const SessionServiceTest&) = delete;
  SessionServiceTest& operator=(const SessionServiceTest&) = delete;

  ~SessionServiceTest() override = default;

 protected:
  void SetUp() override {
    PlatformTest::SetUp();
    ASSERT_TRUE(scoped_temp_directory_.CreateUniqueTempDir());
    directory_ = scoped_temp_directory_.GetPath();

    scoped_refptr<base::SequencedTaskRunner> task_runner =
        base::SingleThreadTaskRunner::GetCurrentDefault();
    session_service_ =
        [[SessionServiceIOS alloc] initWithSaveDelay:kSaveDelay
                                          taskRunner:task_runner];
  }

  void TearDown() override {
    session_service_ = nil;
    PlatformTest::TearDown();
  }

  // Returns a WebStateList with `tabs_count` WebStates and activates the first
  // WebState.
  std::unique_ptr<WebStateList> CreateWebStateList(int tabs_count) {
    std::unique_ptr<WebStateList> web_state_list =
        std::make_unique<WebStateList>(&web_state_list_delegate_);
    for (int i = 0; i < tabs_count; ++i) {
      auto web_state = std::make_unique<web::FakeWebState>();
      web_state->SetNavigationItemCount(1);
      web_state_list->InsertWebState(std::move(web_state),
                                     WebStateList::InsertionParams::AtIndex(i));
    }
    if (tabs_count > 0)
      web_state_list->ActivateWebStateAt(0);
    return web_state_list;
  }

  // Returns the path to serialized SessionWindowIOS from a testdata file named
  // `filename` or nil if the file cannot be found.
  NSString* SessionPathForTestData(const base::FilePath::CharType* filename) {
    base::FilePath session_path;
    if (!base::PathService::Get(ios::DIR_TEST_DATA, &session_path))
      return nil;

    session_path = session_path.Append(FILE_PATH_LITERAL("sessions"));
    session_path = session_path.Append(filename);
    if (!base::PathExists(session_path))
      return nil;

    return base::apple::FilePathToNSString(session_path);
  }

  SessionServiceIOS* session_service() { return session_service_; }

  const base::FilePath& directory() const { return directory_; }

  NSString* directory_as_nsstring() const {
    return base::apple::FilePathToNSString(directory());
  }

  void WaitForBackgroundTaskComplete() {
    base::RunLoop run_loop;
    [session_service_ shutdownWithClosure:run_loop.QuitClosure()];
    run_loop.Run();
  }

  void WaitForSessionSaveComplete() {
    task_environment_.FastForwardBy(kSaveDelay);
    WaitForBackgroundTaskComplete();
  }

 private:
  base::ScopedTempDir scoped_temp_directory_;
  base::test::TaskEnvironment task_environment_;
  SessionServiceIOS* session_service_ = nil;
  FakeWebStateListDelegate web_state_list_delegate_;
  base::FilePath directory_;
};

TEST_F(SessionServiceTest, SessionPathForDirectory) {
  const base::FilePath root(FILE_PATH_LITERAL("root"));
  EXPECT_NSEQ(@"root/Sessions/session-id/session.plist",
              [SessionServiceIOS sessionPathForSessionID:@"session-id"
                                               directory:root]);
}

TEST_F(SessionServiceTest, SaveSessionWindowToPath) {
  std::unique_ptr<WebStateList> web_state_list = CreateWebStateList(0);
  SessionWindowIOSFactory* factory = [[SessionWindowIOSFactory alloc]
      initWithWebStateList:web_state_list.get()];

  NSString* session_id = [[NSUUID UUID] UUIDString];
  [session_service() saveSession:factory
                       sessionID:session_id
                       directory:directory()
                     immediately:YES];

  // Even if `immediately` is YES, the file is created by a task on the task
  // runner passed to SessionServiceIOS initializer (which is the current
  // thread task runner during test). Wait for the task to complete.
  WaitForBackgroundTaskComplete();

  NSFileManager* file_manager = [NSFileManager defaultManager];
  EXPECT_TRUE([file_manager removeItemAtPath:directory_as_nsstring()
                                       error:nullptr]);
}

TEST_F(SessionServiceTest, SaveSessionWindowToPathDirectoryExists) {
  ASSERT_TRUE([[NSFileManager defaultManager]
            createDirectoryAtPath:directory_as_nsstring()
      withIntermediateDirectories:YES
                       attributes:nil
                            error:nullptr]);
  std::unique_ptr<WebStateList> web_state_list = CreateWebStateList(0);
  SessionWindowIOSFactory* factory = [[SessionWindowIOSFactory alloc]
      initWithWebStateList:web_state_list.get()];

  NSString* session_id = [[NSUUID UUID] UUIDString];
  [session_service() saveSession:factory
                       sessionID:session_id
                       directory:directory()
                     immediately:YES];

  // Even if `immediately` is YES, the file is created by a task on the task
  // runner passed to SessionServiceIOS initializer (which is the current
  // thread task runner during test). Wait for the task to complete.
  WaitForBackgroundTaskComplete();

  NSFileManager* file_manager = [NSFileManager defaultManager];
  EXPECT_TRUE([file_manager removeItemAtPath:directory_as_nsstring()
                                       error:nullptr]);
}

TEST_F(SessionServiceTest, LoadSessionFromDirectoryNoFile) {
  NSString* session_id = [[NSUUID UUID] UUIDString];
  SessionWindowIOS* session =
      [session_service() loadSessionWithSessionID:session_id
                                        directory:directory()];
  EXPECT_TRUE(session == nil);
}

// Tests that the session service doesn't retain the SessionWindowIOSFactory,
// and that SaveSession will be no-op if the factory is destroyed earlier.
TEST_F(SessionServiceTest, SaveExpiredSession) {
  std::unique_ptr<WebStateList> web_state_list = CreateWebStateList(2);
  SessionWindowIOSFactory* factory = [[SessionWindowIOSFactory alloc]
      initWithWebStateList:web_state_list.get()];

  NSString* session_id = [[NSUUID UUID] UUIDString];
  [session_service() saveSession:factory
                       sessionID:session_id
                       directory:directory()
                     immediately:NO];
  [factory disconnect];
  factory = nil;

  // Make sure that the delay for saving a session has passed.
  WaitForSessionSaveComplete();

  SessionWindowIOS* session =
      [session_service() loadSessionWithSessionID:session_id
                                        directory:directory()];
  EXPECT_FALSE(session);
}

TEST_F(SessionServiceTest, LoadSessionFromDirectory) {
  std::unique_ptr<WebStateList> web_state_list = CreateWebStateList(2);
  SessionWindowIOSFactory* factory = [[SessionWindowIOSFactory alloc]
      initWithWebStateList:web_state_list.get()];

  NSString* session_id = [[NSUUID UUID] UUIDString];
  [session_service() saveSession:factory
                       sessionID:session_id
                       directory:directory()
                     immediately:YES];

  // Even if `immediately` is YES, the file is created by a task on the task
  // runner passed to SessionServiceIOS initializer (which is the current
  // thread task runner during test). Wait for the task to complete.
  WaitForBackgroundTaskComplete();

  SessionWindowIOS* session_window =
      [session_service() loadSessionWithSessionID:session_id
                                        directory:directory()];
  EXPECT_EQ(2u, session_window.sessions.count);
  EXPECT_EQ(0u, session_window.selectedIndex);
}

TEST_F(SessionServiceTest, LoadSessionFromPath) {
  std::unique_ptr<WebStateList> web_state_list = CreateWebStateList(2);
  SessionWindowIOSFactory* factory = [[SessionWindowIOSFactory alloc]
      initWithWebStateList:web_state_list.get()];

  NSString* session_id = [[NSUUID UUID] UUIDString];
  [session_service() saveSession:factory
                       sessionID:session_id
                       directory:directory()
                     immediately:YES];

  // Even if `immediately` is YES, the file is created by a task on the task
  // runner passed to SessionServiceIOS initializer (which is the current
  // thread task runner during test). Wait for the task to complete.
  WaitForBackgroundTaskComplete();

  NSString* session_path =
      [SessionServiceIOS sessionPathForSessionID:session_id
                                       directory:directory()];
  NSString* renamed_path = [session_path stringByAppendingPathExtension:@"bak"];
  ASSERT_NSNE(session_path, renamed_path);

  // Rename the file.
  ASSERT_TRUE([[NSFileManager defaultManager] moveItemAtPath:session_path
                                                      toPath:renamed_path
                                                       error:nil]);

  SessionWindowIOS* session_window =
      [session_service() loadSessionFromPath:renamed_path];
  EXPECT_EQ(2u, session_window.sessions.count);
  EXPECT_EQ(0u, session_window.selectedIndex);
}

TEST_F(SessionServiceTest, LoadCorruptedSession) {
  NSString* session_path =
      SessionPathForTestData(FILE_PATH_LITERAL("corrupted.plist"));
  ASSERT_NSNE(nil, session_path);
  SessionWindowIOS* session =
      [session_service() loadSessionFromPath:session_path];
  EXPECT_TRUE(session == nil);
}

// TODO(crbug.com/41284267): remove this once M67 has shipped (i.e. once more
// than a year has passed since the introduction of the compatibility code).
TEST_F(SessionServiceTest, LoadM57Session) {
  NSString* session_path =
      SessionPathForTestData(FILE_PATH_LITERAL("session_m57.plist"));
  ASSERT_NSNE(nil, session_path);
  SessionWindowIOS* session_window =
      [session_service() loadSessionFromPath:session_path];
  EXPECT_NSNE(nil, session_window);
}

// TODO(crbug.com/41284267): remove this once M68 has shipped (i.e. once more
// than a year has passed since the introduction of the compatibility code).
TEST_F(SessionServiceTest, LoadM58Session) {
  NSString* session_path =
      SessionPathForTestData(FILE_PATH_LITERAL("session_m58.plist"));
  ASSERT_NSNE(nil, session_path);
  SessionWindowIOS* session_window =
      [session_service() loadSessionFromPath:session_path];
  EXPECT_NSNE(nil, session_window);
}

}  // anonymous namespace