// 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. #import #import #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/mac/foundation_util.h" #include "base/mac/sdk_forward_declarations.h" #include "base/memory/weak_ptr.h" #include "base/run_loop.h" #include "components/storage_monitor/image_capture_device.h" #include "components/storage_monitor/image_capture_device_manager.h" #include "components/storage_monitor/test_storage_monitor.h" #include "content/public/browser/browser_thread.h" #include "content/public/test/test_browser_thread_bundle.h" #include "testing/gtest/include/gtest/gtest.h" namespace { const char kDeviceId[] = "id"; const char kTestFileContents[] = "test"; } // namespace // Private ICCameraDevice method needed to properly initialize the object. @interface NSObject (PrivateAPIICCameraDevice) - (id)initWithDictionary:(id)properties; @end @interface MockICCameraDevice : ICCameraDevice { @private base::scoped_nsobject allMediaFiles_; } - (void)addMediaFile:(ICCameraFile*)file; @end @implementation MockICCameraDevice - (id)init { if ((self = [super initWithDictionary:[NSDictionary dictionary]])) { } return self; } - (NSString*)mountPoint { return @"mountPoint"; } - (NSString*)name { return @"name"; } - (NSString*)UUIDString { return base::SysUTF8ToNSString(kDeviceId); } - (ICDeviceType)type { return ICDeviceTypeCamera; } - (void)requestOpenSession { } - (void)requestCloseSession { } - (NSArray*)mediaFiles { return allMediaFiles_; } - (void)addMediaFile:(ICCameraFile*)file { if (!allMediaFiles_.get()) allMediaFiles_.reset([[NSMutableArray alloc] init]); [allMediaFiles_ addObject:file]; } // This method does approximately what the internal ImageCapture platform // library is observed to do: take the download save-as filename and mangle // it to attach an extension, then return that new filename to the caller // in the options. - (void)requestDownloadFile:(ICCameraFile*)file options:(NSDictionary*)options downloadDelegate:(id)downloadDelegate didDownloadSelector:(SEL)selector contextInfo:(void*)contextInfo { base::FilePath saveDir(base::SysNSStringToUTF8( [[options objectForKey:ICDownloadsDirectoryURL] path])); std::string saveAsFilename = base::SysNSStringToUTF8([options objectForKey:ICSaveAsFilename]); // It appears that the ImageCapture library adds an extension to the requested // filename. Do that here to require a rename. saveAsFilename += ".jpg"; base::FilePath toBeSaved = saveDir.Append(saveAsFilename); ASSERT_EQ(static_cast(strlen(kTestFileContents)), base::WriteFile(toBeSaved, kTestFileContents, strlen(kTestFileContents))); NSMutableDictionary* returnOptions = [NSMutableDictionary dictionaryWithDictionary:options]; [returnOptions setObject:base::SysUTF8ToNSString(saveAsFilename) forKey:ICSavedFilename]; [static_cast*>(downloadDelegate) didDownloadFile:file error:nil options:returnOptions contextInfo:contextInfo]; } @end @interface MockICCameraFolder : ICCameraFolder { @private base::scoped_nsobject name_; } - (id)initWithName:(NSString*)name; @end @implementation MockICCameraFolder - (id)initWithName:(NSString*)name { if ((self = [super init])) { name_.reset([name retain]); } return self; } - (NSString*)name { return name_; } - (ICCameraFolder*)parentFolder { return nil; } @end @interface MockICCameraFile : ICCameraFile { @private base::scoped_nsobject name_; base::scoped_nsobject date_; base::scoped_nsobject parent_; } - (id)init:(NSString*)name; - (void)setParent:(NSString*)parent; @end @implementation MockICCameraFile - (id)init:(NSString*)name { if ((self = [super init])) { name_.reset([name retain]); date_.reset([[NSDate dateWithNaturalLanguageString:@"12/12/12"] retain]); } return self; } - (void)setParent:(NSString*)parent { parent_.reset([[MockICCameraFolder alloc] initWithName:parent]); } - (ICCameraFolder*)parentFolder { return parent_.get(); } - (NSString*)name { return name_; } - (NSString*)UTI { return base::mac::CFToNSCast(kUTTypeImage); } - (NSDate*)modificationDate { return date_.get(); } - (NSDate*)creationDate { return date_.get(); } - (off_t)fileSize { return 1000; } @end namespace storage_monitor { class TestCameraListener : public ImageCaptureDeviceListener, public base::SupportsWeakPtr { public: TestCameraListener() : completed_(false), removed_(false), last_error_(base::File::FILE_ERROR_INVALID_URL) {} ~TestCameraListener() override {} void ItemAdded(const std::string& name, const base::File::Info& info) override { items_.push_back(name); } void NoMoreItems() override { completed_ = true; } void DownloadedFile(const std::string& name, base::File::Error error) override { EXPECT_TRUE(content::BrowserThread::CurrentlyOn( content::BrowserThread::UI)); downloads_.push_back(name); last_error_ = error; } void DeviceRemoved() override { removed_ = true; } std::vector items() const { return items_; } std::vector downloads() const { return downloads_; } bool completed() const { return completed_; } bool removed() const { return removed_; } base::File::Error last_error() const { return last_error_; } private: std::vector items_; std::vector downloads_; bool completed_; bool removed_; base::File::Error last_error_; }; class ImageCaptureDeviceManagerTest : public testing::Test { public: void SetUp() override { monitor_ = TestStorageMonitor::CreateAndInstall(); } void TearDown() override { TestStorageMonitor::Destroy(); } MockICCameraDevice* AttachDevice(ImageCaptureDeviceManager* manager) { // Ownership will be passed to the device browser delegate. base::scoped_nsobject device( [[MockICCameraDevice alloc] init]); id delegate = manager->device_browser(); [delegate deviceBrowser:nil didAddDevice:device moreComing:NO]; return device.autorelease(); } void DetachDevice(ImageCaptureDeviceManager* manager, ICCameraDevice* device) { id delegate = manager->device_browser(); [delegate deviceBrowser:nil didRemoveDevice:device moreGoing:NO]; } protected: content::TestBrowserThreadBundle thread_bundle_; TestStorageMonitor* monitor_; TestCameraListener listener_; }; TEST_F(ImageCaptureDeviceManagerTest, TestAttachDetach) { ImageCaptureDeviceManager manager; manager.SetNotifications(monitor_->receiver()); ICCameraDevice* device = AttachDevice(&manager); std::vector devices = monitor_->GetAllAvailableStorages(); ASSERT_EQ(1U, devices.size()); EXPECT_EQ(std::string("ic:") + kDeviceId, devices[0].device_id()); DetachDevice(&manager, device); devices = monitor_->GetAllAvailableStorages(); ASSERT_EQ(0U, devices.size()); }; TEST_F(ImageCaptureDeviceManagerTest, OpenCamera) { ImageCaptureDeviceManager manager; manager.SetNotifications(monitor_->receiver()); ICCameraDevice* device = AttachDevice(&manager); EXPECT_FALSE(ImageCaptureDeviceManager::deviceForUUID( "nonexistent")); base::scoped_nsobject camera( [ImageCaptureDeviceManager::deviceForUUID(kDeviceId) retain]); [camera setListener:listener_.AsWeakPtr()]; [camera open]; base::scoped_nsobject picture1( [[MockICCameraFile alloc] init:@"pic1"]); [camera cameraDevice:nil didAddItem:picture1]; base::scoped_nsobject picture2( [[MockICCameraFile alloc] init:@"pic2"]); [camera cameraDevice:nil didAddItem:picture2]; ASSERT_EQ(2U, listener_.items().size()); EXPECT_EQ("pic1", listener_.items()[0]); EXPECT_EQ("pic2", listener_.items()[1]); EXPECT_FALSE(listener_.completed()); [camera deviceDidBecomeReadyWithCompleteContentCatalog:nil]; ASSERT_EQ(2U, listener_.items().size()); EXPECT_TRUE(listener_.completed()); [camera close]; DetachDevice(&manager, device); EXPECT_FALSE(ImageCaptureDeviceManager::deviceForUUID(kDeviceId)); } TEST_F(ImageCaptureDeviceManagerTest, RemoveCamera) { ImageCaptureDeviceManager manager; manager.SetNotifications(monitor_->receiver()); ICCameraDevice* device = AttachDevice(&manager); base::scoped_nsobject camera( [ImageCaptureDeviceManager::deviceForUUID(kDeviceId) retain]); [camera setListener:listener_.AsWeakPtr()]; [camera open]; [camera didRemoveDevice:device]; EXPECT_TRUE(listener_.removed()); } TEST_F(ImageCaptureDeviceManagerTest, DownloadFile) { ImageCaptureDeviceManager manager; manager.SetNotifications(monitor_->receiver()); MockICCameraDevice* device = AttachDevice(&manager); base::scoped_nsobject camera( [ImageCaptureDeviceManager::deviceForUUID(kDeviceId) retain]); [camera setListener:listener_.AsWeakPtr()]; [camera open]; std::string kTestFileName("pic1"); base::scoped_nsobject picture1( [[MockICCameraFile alloc] init:base::SysUTF8ToNSString(kTestFileName)]); [device addMediaFile:picture1]; [camera cameraDevice:nil didAddItem:picture1]; base::ScopedTempDir temp_dir; ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); EXPECT_EQ(0U, listener_.downloads().size()); // Test that a nonexistent file we ask to be downloaded will // return us a not-found error. base::FilePath temp_file = temp_dir.path().Append("tempfile"); [camera downloadFile:std::string("nonexistent") localPath:temp_file]; base::RunLoop().RunUntilIdle(); ASSERT_EQ(1U, listener_.downloads().size()); EXPECT_EQ("nonexistent", listener_.downloads()[0]); EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, listener_.last_error()); // Test that an existing file we ask to be downloaded will end up in // the location we specify. The mock system will copy testing file // contents to a separate filename, mimicking the ImageCaptureCore // library behavior. Our code then renames the file onto the requested // destination. [camera downloadFile:kTestFileName localPath:temp_file]; base::RunLoop().RunUntilIdle(); ASSERT_EQ(2U, listener_.downloads().size()); EXPECT_EQ(kTestFileName, listener_.downloads()[1]); ASSERT_EQ(base::File::FILE_OK, listener_.last_error()); char file_contents[5]; ASSERT_EQ(4, base::ReadFile(temp_file, file_contents, strlen(kTestFileContents))); EXPECT_EQ(kTestFileContents, std::string(file_contents, strlen(kTestFileContents))); [camera didRemoveDevice:device]; } TEST_F(ImageCaptureDeviceManagerTest, TestSubdirectories) { ImageCaptureDeviceManager manager; manager.SetNotifications(monitor_->receiver()); MockICCameraDevice* device = AttachDevice(&manager); base::scoped_nsobject camera( [ImageCaptureDeviceManager::deviceForUUID(kDeviceId) retain]); [camera setListener:listener_.AsWeakPtr()]; [camera open]; std::string kTestFileName("pic1"); base::scoped_nsobject picture1( [[MockICCameraFile alloc] init:base::SysUTF8ToNSString(kTestFileName)]); [picture1 setParent:base::SysUTF8ToNSString("dir")]; [device addMediaFile:picture1]; [camera cameraDevice:nil didAddItem:picture1]; base::ScopedTempDir temp_dir; ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); base::FilePath temp_file = temp_dir.path().Append("tempfile"); [camera downloadFile:("dir/" + kTestFileName) localPath:temp_file]; base::RunLoop().RunUntilIdle(); char file_contents[5]; ASSERT_EQ(4, base::ReadFile(temp_file, file_contents, strlen(kTestFileContents))); EXPECT_EQ(kTestFileContents, std::string(file_contents, strlen(kTestFileContents))); [camera didRemoveDevice:device]; } } // namespace storage_monitor