Commit d1f1b75f authored by kkania@chromium.org's avatar kkania@chromium.org

[chromedriver] Add command for uploading a file to a remote ChromeDriver server,

which can be used when performing a file upload using sendKeys.

Unfortunately, past and current versions of some WebDriver clients,
including java, only upload a zip entry and not an actual zip. This can't be
parsed by chromium's zip utilities or minizip. The WebDriver clients should
be changed to send an actual zip file. In the meantime, we append a central
directory on to the zip entry and then unzip it.
BUG=chromedriver:18
TEST=none


Review URL: http://codereview.chromium.org/9515005

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@125004 0039d316-1c4b-4281-b951-d872f2087c98
parent 66495020
......@@ -1018,6 +1018,8 @@
'test/webdriver/commands/execute_async_script_command.h',
'test/webdriver/commands/execute_command.cc',
'test/webdriver/commands/execute_command.h',
'test/webdriver/commands/file_upload_command.cc',
'test/webdriver/commands/file_upload_command.h',
'test/webdriver/commands/find_element_commands.cc',
'test/webdriver/commands/find_element_commands.h',
'test/webdriver/commands/html5_storage_commands.cc',
......
// Copyright (c) 2012 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 "chrome/test/webdriver/commands/file_upload_command.h"
#include "base/file_util.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/test/webdriver/commands/response.h"
#include "chrome/test/webdriver/webdriver_error.h"
#include "chrome/test/webdriver/webdriver_session.h"
#include "chrome/test/webdriver/webdriver_util.h"
namespace webdriver {
FileUploadCommand::FileUploadCommand(
const std::vector<std::string>& path_segments,
DictionaryValue* parameters)
: WebDriverCommand(path_segments, parameters) {
}
FileUploadCommand::~FileUploadCommand() {
}
bool FileUploadCommand::DoesPost() {
return true;
}
void FileUploadCommand::ExecutePost(Response* const response) {
std::string base64_zip_data;
if (!GetStringParameter("file", &base64_zip_data)) {
response->SetError(new Error(kUnknownError, "Missing or invalid 'file'"));
return;
}
std::string zip_data;
if (!Base64Decode(base64_zip_data, &zip_data)) {
response->SetError(new Error(kUnknownError, "Unable to decode 'file'"));
return;
}
FilePath upload_dir;
if (!file_util::CreateTemporaryDirInDir(
session_->temp_dir(), FILE_PATH_LITERAL("upload"), &upload_dir)) {
response->SetError(new Error(kUnknownError, "Failed to create temp dir"));
return;
}
std::string error_msg;
FilePath upload;
if (!UnzipSoleFile(upload_dir, zip_data, &upload, &error_msg)) {
response->SetError(new Error(kUnknownError, error_msg));
return;
}
#if defined(OS_WIN)
response->SetValue(new base::StringValue(WideToUTF8(upload.value())));
#else
response->SetValue(new base::StringValue(upload.value()));
#endif
}
} // namespace webdriver
// Copyright (c) 2012 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.
#ifndef CHROME_TEST_WEBDRIVER_COMMANDS_FILE_UPLOAD_COMMAND_H_
#define CHROME_TEST_WEBDRIVER_COMMANDS_FILE_UPLOAD_COMMAND_H_
#include <string>
#include <vector>
#include "chrome/test/webdriver/commands/webdriver_command.h"
namespace base {
class DictionaryValue;
}
namespace webdriver {
class Response;
class FileUploadCommand : public WebDriverCommand {
public:
FileUploadCommand(const std::vector<std::string>& path_segments,
base::DictionaryValue* parameters);
virtual ~FileUploadCommand();
virtual bool DoesPost() OVERRIDE;
virtual void ExecutePost(Response* const response) OVERRIDE;
private:
DISALLOW_COPY_AND_ASSIGN(FileUploadCommand);
};
} // namespace webdriver
#endif // CHROME_TEST_WEBDRIVER_COMMANDS_FILE_UPLOAD_COMMAND_H_
......@@ -4,14 +4,12 @@
#include "chrome/test/webdriver/webdriver_capabilities_parser.h"
#include "base/base64.h"
#include "base/file_util.h"
#include "base/format_macros.h"
#include "base/stringprintf.h"
#include "base/string_util.h"
#include "base/values.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/zip.h"
#include "chrome/test/webdriver/webdriver_error.h"
#include "chrome/test/webdriver/webdriver_util.h"
......@@ -202,13 +200,13 @@ Error* CapabilitiesParser::ParseExtensions(const Value* option) {
}
FilePath extension = root_.AppendASCII(
base::StringPrintf("extension%" PRIuS ".crx", i));
std::string error_msg;
if (!DecodeAndWriteFile(extension, extension_base64, false /* unzip */,
&error_msg)) {
return new Error(
kUnknownError,
"Error occurred while parsing extension: " + error_msg);
}
std::string decoded_extension;
if (!Base64Decode(extension_base64, &decoded_extension))
return new Error(kUnknownError, "Failed to base64 decode extension");
int size = static_cast<int>(decoded_extension.length());
if (file_util::WriteFile(
extension, decoded_extension.c_str(), size) != size)
return new Error(kUnknownError, "Failed to write extension file");
caps_->extensions.push_back(extension);
}
return NULL;
......@@ -273,8 +271,7 @@ Error* CapabilitiesParser::ParseProfile(const Value* option) {
return CreateBadInputError("profile", Value::TYPE_STRING, option);
std::string error_msg;
caps_->profile = root_.AppendASCII("profile");
if (!DecodeAndWriteFile(caps_->profile, profile_base64, true /* unzip */,
&error_msg))
if (!Base64DecodeAndUnzip(caps_->profile, profile_base64, &error_msg))
return new Error(kUnknownError, "unable to unpack profile: " + error_msg);
return NULL;
}
......@@ -432,33 +429,4 @@ Error* CapabilitiesParser::ParseNoWebsiteTestingDefaults(const Value* option) {
return NULL;
}
bool CapabilitiesParser::DecodeAndWriteFile(
const FilePath& path,
const std::string& base64,
bool unzip,
std::string* error_msg) {
std::string data;
if (!base::Base64Decode(base64, &data)) {
*error_msg = "Could not decode base64 data";
return false;
}
if (unzip) {
FilePath temp_file(root_.AppendASCII(GenerateRandomID()));
if (!file_util::WriteFile(temp_file, data.c_str(), data.length())) {
*error_msg = "Could not write file";
return false;
}
if (!zip::Unzip(temp_file, path)) {
*error_msg = "Could not unzip archive";
return false;
}
} else {
if (!file_util::WriteFile(path, data.c_str(), data.length())) {
*error_msg = "Could not write file";
return false;
}
}
return true;
}
} // namespace webdriver
......@@ -111,13 +111,6 @@ class CapabilitiesParser {
Error* ParseProxyAutoconfigUrl(const base::DictionaryValue* options);
Error* ParseProxyServers(const base::DictionaryValue* options);
Error* ParseNoWebsiteTestingDefaults(const base::Value* option);
// Decodes the given base64-encoded string, optionally unzips it, and
// writes the result to |path|.
// On error, false will be returned and |error_msg| will be set.
bool DecodeAndWriteFile(const FilePath& path,
const std::string& base64,
bool unzip,
std::string* error_msg);
// The capabilities dictionary to parse.
const base::DictionaryValue* dict_;
......
......@@ -39,6 +39,7 @@
#include "chrome/test/webdriver/commands/create_session.h"
#include "chrome/test/webdriver/commands/execute_async_script_command.h"
#include "chrome/test/webdriver/commands/execute_command.h"
#include "chrome/test/webdriver/commands/file_upload_command.h"
#include "chrome/test/webdriver/commands/find_element_commands.h"
#include "chrome/test/webdriver/commands/html5_storage_commands.h"
#include "chrome/test/webdriver/commands/keys_command.h"
......@@ -148,6 +149,7 @@ void InitCallbacks(Dispatcher* dispatcher,
"/session/*/timeouts/async_script");
dispatcher->Add<ImplicitWaitCommand>( "/session/*/timeouts/implicit_wait");
dispatcher->Add<LogCommand>( "/session/*/log");
dispatcher->Add<FileUploadCommand>( "/session/*/file");
// Cookie functions.
dispatcher->Add<CookieCommand>( "/session/*/cookie");
......
......@@ -18,7 +18,6 @@
#include "base/message_loop_proxy.h"
#include "base/process.h"
#include "base/process_util.h"
#include "base/scoped_temp_dir.h"
#include "base/string_number_conversions.h"
#include "base/string_split.h"
#include "base/string_util.h"
......@@ -88,8 +87,7 @@ Error* Session::Init(const DictionaryValue* capabilities_dict) {
delete this;
return new Error(kUnknownError, "Cannot start session thread");
}
ScopedTempDir temp_dir;
if (!temp_dir.CreateUniqueTempDir()) {
if (!temp_dir_.CreateUniqueTempDir()) {
delete this;
return new Error(
kUnknownError, "Unable to create temp directory for unpacking");
......@@ -98,7 +96,7 @@ Error* Session::Init(const DictionaryValue* capabilities_dict) {
"Initializing session with capabilities " +
JsonStringifyForDisplay(capabilities_dict));
CapabilitiesParser parser(
capabilities_dict, temp_dir.path(), logger_, &capabilities_);
capabilities_dict, temp_dir_.path(), logger_, &capabilities_);
Error* error = parser.Parse();
if (error) {
delete this;
......@@ -1409,6 +1407,10 @@ const Logger& Session::logger() const {
return logger_;
}
const FilePath& Session::temp_dir() const {
return temp_dir_.path();
}
const Capabilities& Session::capabilities() const {
return capabilities_;
}
......
......@@ -12,6 +12,7 @@
#include "base/callback_forward.h"
#include "base/file_path.h"
#include "base/memory/scoped_ptr.h"
#include "base/scoped_temp_dir.h"
#include "base/string16.h"
#include "base/threading/thread.h"
#include "chrome/common/automation_constants.h"
......@@ -383,6 +384,8 @@ class Session {
const Logger& logger() const;
const FilePath& temp_dir() const;
const Capabilities& capabilities() const;
private:
......@@ -471,6 +474,8 @@ class Session {
std::string alert_prompt_text_;
bool has_alert_prompt_text_;
// Temporary directory containing session data.
ScopedTempDir temp_dir_;
Capabilities capabilities_;
// Current state of all modifier keys.
......
......@@ -4,17 +4,22 @@
#include "chrome/test/webdriver/webdriver_util.h"
#include "base/base64.h"
#include "base/basictypes.h"
#include "base/file_util.h"
#include "base/format_macros.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/scoped_ptr.h"
#include "base/rand_util.h"
#include "base/scoped_temp_dir.h"
#include "base/stringprintf.h"
#include "base/string_number_conversions.h"
#include "base/string_split.h"
#include "base/string_util.h"
#include "base/third_party/icu/icu_utf.h"
#include "chrome/common/automation_id.h"
#include "chrome/common/zip.h"
#include "chrome/test/automation/automation_json_requests.h"
using base::DictionaryValue;
......@@ -31,6 +36,360 @@ std::string GenerateRandomID() {
return base::StringPrintf("%016" PRIx64 "%016" PRIx64, msb, lsb);
}
bool Base64Decode(const std::string& base64,
std::string* bytes) {
std::string copy = base64;
// Some WebDriver client base64 encoders follow RFC 1521, which require that
// 'encoded lines be no more than 76 characters long'. Just remove any
// newlines.
RemoveChars(copy, "\n", &copy);
return base::Base64Decode(copy, bytes);
}
namespace {
bool UnzipArchive(const FilePath& unzip_dir,
const std::string& bytes,
std::string* error_msg) {
ScopedTempDir dir;
if (!dir.CreateUniqueTempDir()) {
*error_msg = "Unable to create temp dir";
return false;
}
FilePath archive = dir.path().AppendASCII("temp.zip");
int length = bytes.length();
if (file_util::WriteFile(archive, bytes.c_str(), length) != length) {
*error_msg = "Could not write file to temp dir";
return false;
}
if (!zip::Unzip(archive, unzip_dir)) {
*error_msg = "Could not unzip archive";
return false;
}
return true;
}
} // namespace
bool Base64DecodeAndUnzip(const FilePath& unzip_dir,
const std::string& base64,
std::string* error_msg) {
std::string zip_data;
if (!Base64Decode(base64, &zip_data)) {
*error_msg = "Could not decode base64 zip data";
return false;
}
return UnzipArchive(unzip_dir, zip_data, error_msg);
}
namespace {
// Stream for writing binary data.
class DataOutputStream {
public:
DataOutputStream() {}
~DataOutputStream() {}
void WriteUInt16(uint16 data) {
WriteBytes(&data, sizeof(data));
}
void WriteUInt32(uint32 data) {
WriteBytes(&data, sizeof(data));
}
void WriteString(const std::string& data) {
WriteBytes(data.c_str(), data.length());
}
void WriteBytes(const void* bytes, int size) {
size_t next = buffer_.length();
buffer_.resize(next + size);
memcpy(&buffer_[next], bytes, size);
}
const std::string& buffer() const { return buffer_; }
private:
std::string buffer_;
};
// Stream for reading binary data.
class DataInputStream {
public:
DataInputStream(const char* data, int size)
: data_(data), size_(size), iter_(0) {}
~DataInputStream() {}
bool ReadUInt16(uint16* data) {
return ReadBytes(data, sizeof(*data));
}
bool ReadUInt32(uint32* data) {
return ReadBytes(data, sizeof(*data));
}
bool ReadString(std::string* data, int length) {
if (length < 0)
return false;
// Check here to make sure we don't allocate wastefully.
if (iter_ + length > size_)
return false;
data->resize(length);
return ReadBytes(&(*data)[0], length);
}
bool ReadBytes(void* bytes, int size) {
if (iter_ + size > size_)
return false;
memcpy(bytes, &data_[iter_], size);
iter_ += size;
return true;
}
int remaining() const { return size_ - iter_; }
private:
const char* data_;
int size_;
int iter_;
};
// A file entry within a zip archive. This may be incomplete and is not
// guaranteed to be able to parse all types of zip entries.
// See http://www.pkware.com/documents/casestudies/APPNOTE.TXT for the zip
// file format.
struct ZipEntry {
// The given bytes must contain the whole zip entry and only the entry,
// although the entry may include a data descriptor.
static bool FromBytes(const std::string& bytes, ZipEntry* zip,
std::string* error_msg) {
DataInputStream stream(bytes.c_str(), bytes.length());
uint32 signature;
if (!stream.ReadUInt32(&signature) || signature != kFileHeaderSignature) {
*error_msg = "Invalid file header signature";
return false;
}
if (!stream.ReadUInt16(&zip->version_needed)) {
*error_msg = "Invalid version";
return false;
}
if (!stream.ReadUInt16(&zip->bit_flag)) {
*error_msg = "Invalid bit flag";
return false;
}
if (!stream.ReadUInt16(&zip->compression_method)) {
*error_msg = "Invalid compression method";
return false;
}
if (!stream.ReadUInt16(&zip->mod_time)) {
*error_msg = "Invalid file last modified time";
return false;
}
if (!stream.ReadUInt16(&zip->mod_date)) {
*error_msg = "Invalid file last modified date";
return false;
}
if (!stream.ReadUInt32(&zip->crc)) {
*error_msg = "Invalid crc";
return false;
}
uint32 compressed_size;
if (!stream.ReadUInt32(&compressed_size)) {
*error_msg = "Invalid compressed size";
return false;
}
if (!stream.ReadUInt32(&zip->uncompressed_size)) {
*error_msg = "Invalid compressed size";
return false;
}
uint16 name_length;
if (!stream.ReadUInt16(&name_length)) {
*error_msg = "Invalid name length";
return false;
}
uint16 field_length;
if (!stream.ReadUInt16(&field_length)) {
*error_msg = "Invalid field length";
return false;
}
if (!stream.ReadString(&zip->name, name_length)) {
*error_msg = "Invalid name";
return false;
}
if (!stream.ReadString(&zip->fields, field_length)) {
*error_msg = "Invalid fields";
return false;
}
if (zip->bit_flag & 0x8) {
// Has compressed data and a separate data descriptor.
if (stream.remaining() < 16) {
*error_msg = "Too small for data descriptor";
return false;
}
compressed_size = stream.remaining() - 16;
if (!stream.ReadString(&zip->compressed_data, compressed_size)) {
*error_msg = "Invalid compressed data before descriptor";
return false;
}
if (!stream.ReadUInt32(&signature) ||
signature != kDataDescriptorSignature) {
*error_msg = "Invalid data descriptor signature";
return false;
}
if (!stream.ReadUInt32(&zip->crc)) {
*error_msg = "Invalid crc";
return false;
}
if (!stream.ReadUInt32(&compressed_size)) {
*error_msg = "Invalid compressed size";
return false;
}
if (compressed_size != zip->compressed_data.length()) {
*error_msg = "Compressed data does not match data descriptor";
return false;
}
if (!stream.ReadUInt32(&zip->uncompressed_size)) {
*error_msg = "Invalid compressed size";
return false;
}
} else {
// Just has compressed data.
if (!stream.ReadString(&zip->compressed_data, compressed_size)) {
*error_msg = "Invalid compressed data";
return false;
}
if (stream.remaining() != 0) {
*error_msg = "Leftover data after zip entry";
return false;
}
}
return true;
}
// Returns bytes for a valid zip file that just contains this zip entry.
std::string ToZip() {
// Write zip entry with no data descriptor.
DataOutputStream stream;
stream.WriteUInt32(kFileHeaderSignature);
stream.WriteUInt16(version_needed);
stream.WriteUInt16(bit_flag);
stream.WriteUInt16(compression_method);
stream.WriteUInt16(mod_time);
stream.WriteUInt16(mod_date);
stream.WriteUInt32(crc);
stream.WriteUInt32(compressed_data.length());
stream.WriteUInt32(uncompressed_size);
stream.WriteUInt16(name.length());
stream.WriteUInt16(fields.length());
stream.WriteString(name);
stream.WriteString(fields);
stream.WriteString(compressed_data);
uint32 entry_size = stream.buffer().length();
// Write central directory.
stream.WriteUInt32(kCentralDirSignature);
stream.WriteUInt16(0x14); // Version made by. Unused at version 0.
stream.WriteUInt16(version_needed);
stream.WriteUInt16(bit_flag);
stream.WriteUInt16(compression_method);
stream.WriteUInt16(mod_time);
stream.WriteUInt16(mod_date);
stream.WriteUInt32(crc);
stream.WriteUInt32(compressed_data.length());
stream.WriteUInt32(uncompressed_size);
stream.WriteUInt16(name.length());
stream.WriteUInt16(fields.length());
stream.WriteUInt16(0); // Comment length.
stream.WriteUInt16(0); // Disk number where file starts.
stream.WriteUInt16(0); // Internal file attr.
stream.WriteUInt32(0); // External file attr.
stream.WriteUInt32(0); // Offset to file.
stream.WriteString(name);
stream.WriteString(fields);
uint32 cd_size = stream.buffer().length() - entry_size;
// End of central directory.
stream.WriteUInt32(kEndOfCentralDirSignature);
stream.WriteUInt16(0); // num of this disk
stream.WriteUInt16(0); // disk where cd starts
stream.WriteUInt16(1); // number of cds on this disk
stream.WriteUInt16(1); // total cds
stream.WriteUInt32(cd_size); // size of cd
stream.WriteUInt32(entry_size); // offset of cd
stream.WriteUInt16(0); // comment len
return stream.buffer();
}
static const uint32 kFileHeaderSignature;
static const uint32 kDataDescriptorSignature;
static const uint32 kCentralDirSignature;
static const uint32 kEndOfCentralDirSignature;
uint16 version_needed;
uint16 bit_flag;
uint16 compression_method;
uint16 mod_time;
uint16 mod_date;
uint32 crc;
uint32 uncompressed_size;
std::string name;
std::string fields;
std::string compressed_data;
};
const uint32 ZipEntry::kFileHeaderSignature = 0x04034b50;
const uint32 ZipEntry::kDataDescriptorSignature = 0x08074b50;
const uint32 ZipEntry::kCentralDirSignature = 0x02014b50;
const uint32 ZipEntry::kEndOfCentralDirSignature = 0x06054b50;
bool UnzipEntry(const FilePath& unzip_dir,
const std::string& bytes,
std::string* error_msg) {
ZipEntry entry;
std::string zip_error_msg;
if (!ZipEntry::FromBytes(bytes, &entry, &zip_error_msg)) {
*error_msg = "Error while reading zip entry: " + zip_error_msg;
return false;
}
std::string archive = entry.ToZip();
return UnzipArchive(unzip_dir, archive, error_msg);
}
} // namespace
bool UnzipSoleFile(const FilePath& unzip_dir,
const std::string& bytes,
FilePath* file,
std::string* error_msg) {
std::string archive_error, entry_error;
if (!UnzipArchive(unzip_dir, bytes, &archive_error) &&
!UnzipEntry(unzip_dir, bytes, &entry_error)) {
*error_msg = base::StringPrintf(
"Failed to unzip file: Archive error: (%s) Entry error: (%s)",
archive_error.c_str(), entry_error.c_str());
return false;
}
file_util::FileEnumerator enumerator(unzip_dir, false /* recursive */,
static_cast<file_util::FileEnumerator::FileType>(
file_util::FileEnumerator::FILES |
file_util::FileEnumerator::DIRECTORIES));
FilePath first_file = enumerator.Next();
if (first_file.empty()) {
*error_msg = "Zip contained 0 files";
return false;
}
FilePath second_file = enumerator.Next();
if (!second_file.empty()) {
*error_msg = "Zip contained multiple files";
return false;
}
*file = first_file;
return true;
}
std::string JsonStringify(const Value* value) {
std::string json;
base::JSONWriter::Write(value, false, &json);
......
......@@ -24,6 +24,28 @@ namespace webdriver {
// Generates a random, 32-character hexidecimal ID.
std::string GenerateRandomID();
// Decodes the given base64-encoded string, after removing any newlines,
// which are required in some base64 standards.
// Returns true on success.
bool Base64Decode(const std::string& base64, std::string* bytes);
// Unzip the given zip archive, after base64 decoding, into the given directory.
// Returns true on success.
bool Base64DecodeAndUnzip(const FilePath& unzip_dir,
const std::string& base64,
std::string* error_msg);
// Unzips the sole file contained in the given zip data |bytes| into
// |unzip_dir|. The zip data may be a normal zip archive or a single zip file
// entry. If the unzip successfully produced one file, returns true and sets
// |file| to the unzipped file.
// TODO(kkania): Remove the ability to parse single zip file entries when
// the current versions of all WebDriver clients send actual zip files.
bool UnzipSoleFile(const FilePath& unzip_dir,
const std::string& bytes,
FilePath* file,
std::string* error_msg);
// Returns the equivalent JSON string for the given value.
std::string JsonStringify(const base::Value* value);
......
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Copyright (c) 2012 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 <set>
#include <string>
#include "base/base64.h"
#include "base/file_path.h"
#include "base/file_util.h"
#include "base/scoped_temp_dir.h"
#include "chrome/test/webdriver/webdriver_util.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace webdriver {
TEST(RandomIDTest, CanGenerateSufficientlyRandomIDs) {
std::set<std::string> generated_ids;
for (int i = 0; i < 10000; ++i) {
std::string id = webdriver::GenerateRandomID();
std::string id = GenerateRandomID();
ASSERT_EQ(32u, id.length());
ASSERT_TRUE(generated_ids.end() == generated_ids.find(id))
<< "Generated duplicate ID: " << id
......@@ -19,3 +25,24 @@ TEST(RandomIDTest, CanGenerateSufficientlyRandomIDs) {
generated_ids.insert(id);
}
}
TEST(ZipFileTest, ZipEntryToZipArchive) {
ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
std::string data;
// A zip entry sent from a Java WebDriver client (v2.20) that contains a
// file with the contents "COW\n".
const char* kBase64ZipEntry =
"UEsDBBQACAAIAJpyXEAAAAAAAAAAAAAAAAAEAAAAdGVzdHP2D+"
"cCAFBLBwi/wAzGBgAAAAQAAAA=";
ASSERT_TRUE(base::Base64Decode(kBase64ZipEntry, &data));
FilePath file;
std::string error_msg;
ASSERT_TRUE(UnzipSoleFile(temp_dir.path(), data, &file, &error_msg))
<< error_msg;
std::string contents;
ASSERT_TRUE(file_util::ReadFileToString(file, &contents));
ASSERT_STREQ("COW\n", contents.c_str());
}
} // namespace webdriver
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment