summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2021-11-17 22:34:57 +0100
committerJoel Klinghed <the_jk@spawned.biz>2021-11-17 22:34:57 +0100
commit6232d13f5321b87ddf12a1aa36b4545da45f173d (patch)
tree23f3316470a14136debd9d02f9e920ca2b06f4cc /test
Travel3: Simple image and video display site
Reads the images and videos from filesystem and builds a site in memroy.
Diffstat (limited to 'test')
-rw-r--r--test/file_test.cc64
-rw-r--r--test/file_test.hh45
-rw-r--r--test/mock_timezone.hh15
-rw-r--r--test/mock_transport.hh39
-rw-r--r--test/socket_test.cc168
-rw-r--r--test/socket_test.hh48
-rw-r--r--test/test_args.cc264
-rw-r--r--test/test_buffer.cc282
-rw-r--r--test/test_config.cc167
-rw-r--r--test/test_date.cc101
-rw-r--r--test/test_document.cc71
-rw-r--r--test/test_fcgi_protocol.cc678
-rw-r--r--test/test_geo_json.cc123
-rw-r--r--test/test_hash_method.cc19
-rw-r--r--test/test_hasher.cc82
-rw-r--r--test/test_htmlutil.cc26
-rw-r--r--test/test_http_protocol.cc470
-rw-r--r--test/test_image.cc206
-rw-r--r--test/test_jsutil.cc15
-rw-r--r--test/test_mime_types.cc12
-rw-r--r--test/test_observer_list.cc119
-rw-r--r--test/test_pathutil.cc30
-rw-r--r--test/test_signal_handler.cc63
-rw-r--r--test/test_strutil.cc219
-rw-r--r--test/test_tag.cc63
-rw-r--r--test/test_task_runner.cc174
-rw-r--r--test/test_transport_fcgi.cc490
-rw-r--r--test/test_transport_http.cc241
-rw-r--r--test/test_tz_info.cc114
-rw-r--r--test/test_tz_str.cc97
-rw-r--r--test/test_urlutil.cc95
-rw-r--r--test/test_video.cc261
32 files changed, 4861 insertions, 0 deletions
diff --git a/test/file_test.cc b/test/file_test.cc
new file mode 100644
index 0000000..b2f4f05
--- /dev/null
+++ b/test/file_test.cc
@@ -0,0 +1,64 @@
+#include "common.hh"
+
+#include "file_test.hh"
+#include "io.hh"
+#include "str_buffer.hh"
+
+FileTest::FileTest() = default;
+
+FileTest::~FileTest() {
+ if (!path_.empty()) {
+ std::error_code err;
+ std::filesystem::remove(path_, err);
+ }
+}
+
+std::string const& FileTest::extension() {
+ static std::string empty;
+ return empty;
+}
+
+void FileTest::SetUp() {
+ fd_ = create_temp_file(extension(), &path_);
+ ASSERT_FALSE(path_.empty());
+}
+
+void FileTest::write(std::string_view content) {
+ ASSERT_TRUE(fd_);
+ auto buffer = make_strbuffer(content);
+ while (!buffer->empty()) {
+ ASSERT_TRUE(io::drain(buffer.get(), fd_.get()));
+ }
+ close();
+}
+
+void FileTest::close() {
+ if (fd_) {
+ ASSERT_TRUE(io::close(fd_.release()));
+ }
+}
+
+unique_fd FileTest::create_temp_file(std::string const& extension,
+ std::filesystem::path* path) {
+ std::error_code err;
+ auto tmpdir = std::filesystem::temp_directory_path(err);
+ if (tmpdir.empty())
+ return unique_fd();
+ unique_fd ret;
+ for (uint8_t i = 0; i < 0xff; ++i) {
+ char name[50];
+ snprintf(name, sizeof(name), "test-%u%s", i, extension.c_str());
+ auto test = tmpdir / name;
+ ret = io::open(test, io::open_flags::wronly | io::open_flags::create |
+ io::open_flags::excl,
+ std::filesystem::perms::owner_read |
+ std::filesystem::perms::owner_write);
+ if (ret) {
+ *path = test;
+ break;
+ }
+ if (errno != EEXIST)
+ break;
+ }
+ return ret;
+}
diff --git a/test/file_test.hh b/test/file_test.hh
new file mode 100644
index 0000000..af66dfc
--- /dev/null
+++ b/test/file_test.hh
@@ -0,0 +1,45 @@
+#ifndef FILE_TEST_HH
+#define FILE_TEST_HH
+
+#include "unique_fd.hh"
+
+#include <filesystem>
+#include <gtest/gtest.h>
+#include <utility>
+
+class FileTest : public testing::Test {
+public:
+ static unique_fd create_temp_file(std::string const& extension,
+ std::filesystem::path* path);
+
+protected:
+ FileTest();
+ ~FileTest() override;
+
+ void SetUp() override;
+
+ std::filesystem::path const& path() const {
+ return path_;
+ }
+
+ void write(std::string_view content);
+
+ int fd() const {
+ return fd_.get();
+ }
+
+ unique_fd release() {
+ return std::move(fd_);
+ }
+
+ void close();
+
+protected:
+ virtual std::string const& extension();
+
+private:
+ std::filesystem::path path_;
+ unique_fd fd_;
+};
+
+#endif // FILE_TEST_HH
diff --git a/test/mock_timezone.hh b/test/mock_timezone.hh
new file mode 100644
index 0000000..b6cb22d
--- /dev/null
+++ b/test/mock_timezone.hh
@@ -0,0 +1,15 @@
+#ifndef MOCK_TIMEZONE_HH
+#define MOCK_TIMEZONE_HH
+
+#include "timezone.hh"
+
+#include <gmock/gmock.h>
+
+class MockTimezone : public Timezone {
+public:
+ MOCK_METHOD(std::optional<time_t>, get_local_time,
+ (double, double, time_t),
+ (const override));
+};
+
+#endif // MOCK_TIMEZONE_HH
diff --git a/test/mock_transport.hh b/test/mock_transport.hh
new file mode 100644
index 0000000..7fe9235
--- /dev/null
+++ b/test/mock_transport.hh
@@ -0,0 +1,39 @@
+#ifndef MOCK_TRANSPORT_HH
+#define MOCK_TRANSPORT_HH
+
+#include "transport.hh"
+
+#include <gmock/gmock.h>
+
+class MockTransport : public Transport {
+public:
+ std::unique_ptr<Response> create_data(uint16_t code, std::string data)
+ override {
+ return std::unique_ptr<Response>(create_data_proxy(code, std::move(data)));
+ }
+
+ MOCK_METHOD(std::unique_ptr<Response>, create_file,
+ (uint16_t, std::filesystem::path),
+ (override));
+ MOCK_METHOD(std::unique_ptr<Response>, create_exif_thumbnail,
+ (uint16_t, std::filesystem::path),
+ (override));
+ MOCK_METHOD(void, add_client, (unique_fd&&), (override));
+
+ MOCK_METHOD(Response*, create_data_proxy, (uint16_t, std::string));
+};
+
+class MockResponse : public Transport::Response {
+public:
+ MOCK_METHOD(uint16_t, code, (), (override, const));
+ MOCK_METHOD((std::vector<std::pair<std::string, std::string>> const&),
+ headers, (), (override, const));
+ MOCK_METHOD(std::unique_ptr<Transport::Input>, open_content, (), (override));
+ MOCK_METHOD(void, open_content_async, (
+ std::shared_ptr<TaskRunner>,
+ std::function<void(std::unique_ptr<Transport::Input>)>),
+ (override));
+ MOCK_METHOD(void, add_header, (std::string, std::string), (override));
+};
+
+#endif // MOCK_TRANSPORT_HH
diff --git a/test/socket_test.cc b/test/socket_test.cc
new file mode 100644
index 0000000..f50d306
--- /dev/null
+++ b/test/socket_test.cc
@@ -0,0 +1,168 @@
+#include "common.hh"
+
+#include "buffer.hh"
+#include "io.hh"
+#include "looper.hh"
+#include "socket_test.hh"
+
+#include <sys/socket.h>
+#include <utility>
+
+namespace {
+
+constexpr size_t kBaseSize = 512 * 1024;
+constexpr size_t kMaxSize = 10 * 1024 * 1024;
+
+class ClientImpl : public SocketTest::Client {
+public:
+ ClientImpl(std::shared_ptr<Looper> looper, unique_fd&& fd)
+ : looper_(std::move(looper)), fd_(std::move(fd)),
+ in_(Buffer::growing(kBaseSize, kMaxSize)),
+ out_(Buffer::growing(kBaseSize, kMaxSize)) {
+ looper_->add(fd_.get(), Looper::EVENT_READ,
+ std::bind(&ClientImpl::event, this, std::placeholders::_1));
+ }
+
+ ~ClientImpl() override {
+ if (!closed_)
+ looper_->remove(fd_.get());
+ }
+
+ bool closed() const override {
+ return closed_;
+ }
+
+ void write(std::function<void(Buffer*)> writer) override {
+ ASSERT_FALSE(closed_);
+ bool empty = out_->empty();
+ writer(out_.get());
+ if (empty) {
+ size_t bytes;
+ ASSERT_TRUE(io::drain(out_.get(), fd_.get(), &bytes));
+ if (out_->empty())
+ return;
+ }
+ update();
+ }
+
+ void write(std::string_view data) override {
+ ASSERT_FALSE(closed_);
+ if (data.empty())
+ return;
+ bool empty = out_->empty();
+ auto size = Buffer::write(out_.get(), data.data(), data.size());
+ ASSERT_EQ(data.size(), size);
+ if (empty) {
+ size_t bytes;
+ ASSERT_TRUE(io::drain(out_.get(), fd_.get(), &bytes));
+ if (out_->empty())
+ return;
+ }
+ update();
+ }
+
+ void read(std::function<void(RoBuffer*)> reader) override {
+ bool full = in_->full();
+
+ reader(in_.get());
+
+ if (full && !in_->full())
+ update();
+ }
+
+ std::string_view received() const override {
+ size_t avail;
+ auto* ptr = in_->rbuf(1, avail);
+ return std::string_view(ptr, avail);
+ }
+
+ void forget(size_t bytes) override {
+ bool full = in_->full();
+ in_->rcommit(bytes);
+
+ if (full && bytes > 0)
+ update();
+ }
+
+ void wait(Logger* logger) override {
+ ASSERT_FALSE(waiting_);
+ ASSERT_FALSE(closed_);
+ waiting_ = true;
+ looper_->run(logger);
+ waiting_ = false;
+ }
+
+private:
+ void event(uint8_t ev) {
+ if (ev & Looper::EVENT_ERROR) {
+ FAIL();
+ }
+ bool need_update = false;
+ if (ev & Looper::EVENT_READ) {
+ switch (io::fill(fd_.get(), in_.get())) {
+ case io::Return::OK:
+ break;
+ case io::Return::ERR:
+ FAIL();
+ case io::Return::CLOSED:
+ ASSERT_TRUE(out_->empty());
+ closed_ = true;
+ looper_->remove(fd_.get());
+ fd_.reset();
+ if (waiting_)
+ looper_->quit();
+ return;
+ }
+ if (in_->full())
+ need_update = true;
+ }
+ if (ev & Looper::EVENT_WRITE) {
+ ASSERT_TRUE(io::drain(out_.get(), fd_.get()));
+ if (out_->empty())
+ need_update = true;
+ }
+
+ if (waiting_)
+ looper_->quit();
+
+ if (need_update)
+ update();
+ }
+
+ void update() {
+ uint8_t events = 0;
+ if (!in_->full())
+ events |= Looper::EVENT_READ;
+ if (!out_->empty())
+ events |= Looper::EVENT_WRITE;
+ looper_->update(fd_.get(), events);
+ }
+
+ std::shared_ptr<Looper> looper_;
+ unique_fd fd_;
+ std::unique_ptr<Buffer> in_;
+ std::unique_ptr<Buffer> out_;
+ bool closed_{false};
+ bool waiting_{false};
+};
+
+} // namespace
+
+SocketTest::SocketTest()
+ : looper_(Looper::create()) {}
+
+std::pair<unique_fd, unique_fd> SocketTest::create_pair() {
+ int ret[2];
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, ret) == 0) {
+ if (io::make_nonblocking(ret[0]) && io::make_nonblocking(ret[1])) {
+ return std::make_pair(unique_fd(ret[0]), unique_fd(ret[1]));
+ }
+ io::close(ret[0]);
+ io::close(ret[1]);
+ }
+ return std::make_pair(nullptr, nullptr);
+}
+
+std::unique_ptr<SocketTest::Client> SocketTest::create_client(unique_fd&& fd) {
+ return std::make_unique<ClientImpl>(looper_, std::move(fd));
+}
diff --git a/test/socket_test.hh b/test/socket_test.hh
new file mode 100644
index 0000000..de71619
--- /dev/null
+++ b/test/socket_test.hh
@@ -0,0 +1,48 @@
+#ifndef SOCKET_TEST_HH
+#define SOCKET_TEST_HH
+
+#include "unique_fd.hh"
+
+#include <gtest/gtest.h>
+#include <utility>
+
+class Buffer;
+class Logger;
+class Looper;
+class RoBuffer;
+
+class SocketTest : public testing::Test {
+public:
+ class Client {
+ public:
+ virtual ~Client() = default;
+
+ virtual void write(std::string_view data) = 0;
+ virtual void write(std::function<void(Buffer*)> writer) = 0;
+ virtual std::string_view received() const = 0;
+ virtual void forget(size_t bytes) = 0;
+ virtual void read(std::function<void(RoBuffer*)> reader) = 0;
+ virtual bool closed() const = 0;
+
+ virtual void wait(Logger* logger) = 0;
+
+ protected:
+ Client() = default;
+ Client(Client const&) = delete;
+ Client& operator=(Client const&) = delete;
+ };
+
+protected:
+ SocketTest();
+
+ std::pair<unique_fd, unique_fd> create_pair();
+
+ std::unique_ptr<Client> create_client(unique_fd&& fd);
+
+ std::shared_ptr<Looper> const& looper() { return looper_; }
+
+private:
+ std::shared_ptr<Looper> looper_;
+};
+
+#endif // SOCKET_TEST_HH
diff --git a/test/test_args.cc b/test/test_args.cc
new file mode 100644
index 0000000..198323a
--- /dev/null
+++ b/test/test_args.cc
@@ -0,0 +1,264 @@
+#include "common.hh"
+
+#include "args.hh"
+
+#include <gtest/gtest.h>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace {
+
+bool run(Args* args, std::vector<std::string> const& argv,
+ std::ostream& err, std::vector<std::string>* out) {
+ auto ptrs = std::make_unique<char*[]>(argv.size());
+ for (size_t i = 0; i < argv.size(); ++i)
+ ptrs[i] = const_cast<char*>(argv[i].c_str());
+ return args->run(argv.size(), ptrs.get(),
+ argv.empty() ? "test" : argv[0], err, out);
+}
+
+} // namespace
+
+TEST(args, empty) {
+ auto args = Args::create();
+ std::vector<std::string> argv;
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_TRUE(run(args.get(), argv, err, &arguments));
+ EXPECT_TRUE(err.str().empty());
+ EXPECT_TRUE(arguments.empty());
+}
+
+TEST(args, only_arguments) {
+ auto args = Args::create();
+ auto* help = args->add_option('h', "help", "");
+ auto* config = args->add_option_with_arg('C', "config", "", "");
+ std::vector<std::string> argv{"test", "foo", "bar"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_TRUE(run(args.get(), argv, err, &arguments));
+ EXPECT_TRUE(err.str().empty());
+ EXPECT_FALSE(help->is_set());
+ EXPECT_FALSE(config->is_set());
+ ASSERT_EQ(2, arguments.size());
+ EXPECT_EQ("foo", arguments[0]);
+ EXPECT_EQ("bar", arguments[1]);
+}
+
+TEST(args, everything) {
+ auto args = Args::create();
+ auto* help = args->add_option('h', "help", "");
+ auto* config = args->add_option_with_arg('C', "config", "", "");
+ std::vector<std::string> argv{"test", "--help", "-C", "config.txt", "arg"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_TRUE(run(args.get(), argv, err, &arguments));
+ EXPECT_TRUE(err.str().empty());
+ EXPECT_TRUE(help->is_set());
+ EXPECT_TRUE(config->is_set());
+ EXPECT_EQ("config.txt", config->arg());
+ ASSERT_EQ(1, arguments.size());
+ EXPECT_EQ("arg", arguments[0]);
+}
+
+TEST(args, option_with_arg_short) {
+ auto args = Args::create();
+ auto* config = args->add_option_with_arg('C', "config", "", "");
+ std::vector<std::string> argv{"test", "-C", "config.txt"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_TRUE(run(args.get(), argv, err, &arguments));
+ EXPECT_TRUE(err.str().empty());
+ EXPECT_TRUE(config->is_set());
+ EXPECT_EQ("config.txt", config->arg());
+ EXPECT_TRUE(arguments.empty());
+}
+
+TEST(args, option_with_arg_long_1) {
+ auto args = Args::create();
+ auto* config = args->add_option_with_arg('C', "config", "", "");
+ std::vector<std::string> argv{"test", "--config", "config.txt"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_TRUE(run(args.get(), argv, err, &arguments));
+ EXPECT_TRUE(err.str().empty());
+ EXPECT_TRUE(config->is_set());
+ EXPECT_EQ("config.txt", config->arg());
+ EXPECT_TRUE(arguments.empty());
+}
+
+TEST(args, option_with_arg_long_2) {
+ auto args = Args::create();
+ auto* config = args->add_option_with_arg('C', "config", "", "");
+ std::vector<std::string> argv{"test", "--config=config.txt"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_TRUE(run(args.get(), argv, err, &arguments));
+ EXPECT_TRUE(err.str().empty());
+ EXPECT_TRUE(config->is_set());
+ EXPECT_EQ("config.txt", config->arg());
+ EXPECT_TRUE(arguments.empty());
+}
+
+TEST(args, multiple_options_with_args) {
+ auto args = Args::create();
+ auto* config = args->add_option_with_arg('C', "config", "", "");
+ auto* pid = args->add_option_with_arg('p', "pid", "", "");
+ std::vector<std::string> argv{"test", "-Cp", "config.txt", "pid"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_TRUE(run(args.get(), argv, err, &arguments));
+ EXPECT_TRUE(err.str().empty());
+ EXPECT_TRUE(config->is_set());
+ EXPECT_EQ("config.txt", config->arg());
+ EXPECT_TRUE(pid->is_set());
+ EXPECT_EQ("pid", pid->arg());
+ EXPECT_TRUE(arguments.empty());
+}
+
+TEST(args, end_of_options) {
+ auto args = Args::create();
+ auto* config = args->add_option_with_arg('C', "config", "", "");
+ std::vector<std::string> argv{"test", "--", "--config", "-C"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_TRUE(run(args.get(), argv, err, &arguments));
+ EXPECT_TRUE(err.str().empty());
+ EXPECT_FALSE(config->is_set());
+ ASSERT_EQ(2, arguments.size());
+ EXPECT_EQ("--config", arguments[0]);
+ EXPECT_EQ("-C", arguments[1]);
+}
+
+TEST(args, unknown_long_option) {
+ auto args = Args::create();
+ std::vector<std::string> argv{"test", "--help"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_FALSE(run(args.get(), argv, err, &arguments));
+ EXPECT_EQ("test: unrecognized option '--help'\n", err.str());
+}
+
+TEST(args, unknown_short_option) {
+ auto args = Args::create();
+ std::vector<std::string> argv{"test", "-H"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_FALSE(run(args.get(), argv, err, &arguments));
+ EXPECT_EQ("test: invalid option -- 'H'\n", err.str());
+}
+
+TEST(args, option_not_expecting_argument) {
+ auto args = Args::create();
+ args->add_option('h', "help", "");
+ std::vector<std::string> argv{"test", "--help=foo"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_FALSE(run(args.get(), argv, err, &arguments));
+ EXPECT_EQ("test: option '--help' doesn't allow an argument\n", err.str());
+}
+
+TEST(args, short_option_expecting_argument) {
+ auto args = Args::create();
+ args->add_option_with_arg('C', "config", "", "");
+ std::vector<std::string> argv{"test", "-C"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_FALSE(run(args.get(), argv, err, &arguments));
+ EXPECT_EQ("test: option requires an argument -- 'C'\n", err.str());
+}
+
+TEST(args, long_option_expecting_argument) {
+ auto args = Args::create();
+ args->add_option_with_arg('C', "config", "", "");
+ std::vector<std::string> argv{"test", "--config"};
+ std::stringstream err;
+ std::vector<std::string> arguments;
+ EXPECT_FALSE(run(args.get(), argv, err, &arguments));
+ EXPECT_EQ("test: option '--config' requires an argument\n", err.str());
+}
+
+TEST(args, descriptions) {
+ auto args = Args::create();
+ args->add_option('h', "help", "display this text and exit.");
+ args->add_option_with_arg(
+ 'C', "config", "use FILE instead of the default", "FILE");
+ std::stringstream out;
+ // if the text fits it is (which is does) 80 is always used.
+ args->print_descriptions(out, 60);
+ EXPECT_EQ(" -h, --help "
+ "display this text and exit.\n"
+ " -C, --config=FILE "
+ "use FILE instead of the default\n",
+ out.str());
+}
+
+TEST(args, descriptions_only_long_or_short) {
+ auto args = Args::create();
+ args->add_option('\0', "help", "display this text and exit.");
+ args->add_option_with_arg(
+ 'C', "", "use FILE instead of the default", "FILE");
+ std::stringstream out;
+ // if the text fits it is (which is does) 80 is always used.
+ args->print_descriptions(out, 60);
+ EXPECT_EQ(" --help "
+ "display this text and exit.\n"
+ " -C FILE "
+ "use FILE instead of the default\n",
+ out.str());
+}
+
+TEST(args, descriptions_wrap) {
+ auto args = Args::create();
+ args->add_option('h', "help", "display this text and exit.");
+ args->add_option_with_arg(
+ 'C', "config", "use FILE instead of the default", "FILE");
+ std::stringstream out;
+ args->print_descriptions(out, 45);
+ EXPECT_EQ(" -h, --help display this text and\n"
+ " exit.\n"
+ " -C, --config=FILE use FILE instead of the\n"
+ " default\n",
+ out.str());
+}
+
+TEST(args, descriptions_multiline_wrap) {
+ auto args = Args::create();
+ args->add_option('h', "help", "display this text and exit."
+ " Or not, I'm not the boss of you."
+ " But you really should be doing something with your life.");
+ std::stringstream out;
+ args->print_descriptions(out, 45);
+ EXPECT_EQ(" -h, --help display this text and exit. Or\n"
+ " not, I'm not the boss of you.\n"
+ " But you really should be doing\n"
+ " something with your life.\n",
+ out.str());
+}
+
+TEST(args, descriptions_wrap_too_long_word) {
+ auto args = Args::create();
+ args->add_option('h', "help",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ std::stringstream out;
+ args->print_descriptions(out, 40);
+ EXPECT_EQ(" -h, --help aaaaaaaaaaaaaaaaaaaaaaaaaa\n"
+ " aaaaaaaaaaaaaaaaaaa\n",
+ out.str());
+}
+
+TEST(args, descriptions_fallback) {
+ auto args = Args::create();
+ args->add_option('h', "help", "display this text and exit.");
+ args->add_option_with_arg(
+ 'C', "config", "use FILE instead of the default", "FILE");
+ std::stringstream out;
+ args->print_descriptions(out, 40);
+ EXPECT_EQ("-h, --help\n"
+ "display this text and exit.\n"
+ "-C, --config=FILE\n"
+ "use FILE instead of the default\n",
+ out.str());
+}
diff --git a/test/test_buffer.cc b/test/test_buffer.cc
new file mode 100644
index 0000000..f1623c6
--- /dev/null
+++ b/test/test_buffer.cc
@@ -0,0 +1,282 @@
+#include "common.hh"
+
+#include <algorithm>
+#include <gtest/gtest.h>
+
+#include "buffer.hh"
+
+TEST(buffer, fixed) {
+ auto buf = Buffer::fixed(10);
+ EXPECT_TRUE(buf->empty());
+ EXPECT_FALSE(buf->full());
+ size_t avail;
+ buf->rbuf(0, avail);
+ EXPECT_EQ(0, avail);
+ buf->wbuf(1, avail);
+ EXPECT_GE(avail, 1);
+ auto* wptr = buf->wbuf(100, avail);
+ EXPECT_EQ(10, avail);
+ avail = std::min(static_cast<size_t>(5), avail);
+ std::fill_n(wptr, avail, 'A');
+ buf->wcommit(avail);
+ EXPECT_FALSE(buf->empty());
+ EXPECT_FALSE(buf->full());
+ auto* rptr = buf->rbuf(0, avail);
+ EXPECT_EQ(5, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ('A', rptr[i]) << i;
+ buf->rcommit(2);
+ wptr = buf->wbuf(7, avail);
+ EXPECT_EQ(7, avail);
+ avail = std::min(static_cast<size_t>(7), avail);
+ std::fill_n(wptr, avail, 'B');
+ buf->wcommit(avail);
+ EXPECT_FALSE(buf->empty());
+ EXPECT_TRUE(buf->full());
+ rptr = buf->rbuf(0, avail);
+ EXPECT_EQ(10, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ(i < 3 ? 'A' : 'B', rptr[i]) << i;
+ buf->rcommit(5);
+ EXPECT_FALSE(buf->empty());
+ EXPECT_FALSE(buf->full());
+ wptr = buf->wbuf(5, avail);
+ EXPECT_EQ(5, avail);
+ avail = std::min(static_cast<size_t>(5), avail);
+ std::fill_n(wptr, avail, 'C');
+ buf->wcommit(avail);
+ EXPECT_FALSE(buf->empty());
+ EXPECT_TRUE(buf->full());
+ rptr = buf->rbuf(0, avail);
+ if (avail == 10) {
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ(i < 5 ? 'B' : 'C', rptr[i]) << i;
+ buf->rcommit(10);
+ } else {
+ EXPECT_EQ(5, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ('B', rptr[i]) << i;
+ buf->rcommit(5);
+ rptr = buf->rbuf(0, avail);
+ EXPECT_EQ(5, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ('C', rptr[i]) << i;
+ buf->rcommit(5);
+ }
+ buf->rcommit(0);
+ EXPECT_TRUE(buf->empty());
+ EXPECT_FALSE(buf->full());
+
+ buf->wbuf(10, avail);
+ EXPECT_EQ(10, avail);
+ buf->wcommit(10);
+ buf->wbuf(10, avail);
+ EXPECT_EQ(0, avail);
+ buf->wcommit(0);
+ buf->clear();
+ buf->wbuf(10, avail);
+ EXPECT_EQ(10, avail);
+}
+
+TEST(buffer, growing) {
+ auto buf = Buffer::growing(5, 50);
+ EXPECT_TRUE(buf->empty());
+ EXPECT_FALSE(buf->full());
+ size_t avail;
+ buf->rbuf(0, avail);
+ EXPECT_EQ(0, avail);
+ buf->wbuf(1, avail);
+ EXPECT_GE(avail, 1);
+ auto* wptr = buf->wbuf(100, avail);
+ EXPECT_EQ(50, avail);
+ avail = std::min(static_cast<size_t>(5), avail);
+ std::fill_n(wptr, avail, 'A');
+ buf->wcommit(avail);
+ EXPECT_FALSE(buf->empty());
+ EXPECT_FALSE(buf->full());
+ auto* rptr = buf->rbuf(0, avail);
+ EXPECT_EQ(5, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ('A', rptr[i]) << i;
+ buf->rcommit(2);
+ wptr = buf->wbuf(7, avail);
+ EXPECT_GE(avail, 7);
+ avail = std::min(static_cast<size_t>(7), avail);
+ std::fill_n(wptr, avail, 'B');
+ buf->wcommit(avail);
+ EXPECT_FALSE(buf->empty());
+ EXPECT_FALSE(buf->full());
+ rptr = buf->rbuf(0, avail);
+ EXPECT_EQ(10, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ(i < 3 ? 'A' : 'B', rptr[i]) << i;
+ buf->rcommit(5);
+ EXPECT_FALSE(buf->empty());
+ EXPECT_FALSE(buf->full());
+ wptr = buf->wbuf(5, avail);
+ EXPECT_GE(avail, 5);
+ avail = std::min(static_cast<size_t>(5), avail);
+ std::fill_n(wptr, avail, 'C');
+ buf->wcommit(avail);
+ EXPECT_FALSE(buf->empty());
+ EXPECT_FALSE(buf->full());
+ rptr = buf->rbuf(0, avail);
+ EXPECT_EQ(10, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ(i < 5 ? 'B' : 'C', rptr[i]) << i;
+ buf->rcommit(10);
+ buf->rcommit(0);
+ EXPECT_TRUE(buf->empty());
+ EXPECT_FALSE(buf->full());
+
+ buf->wbuf(50, avail);
+ EXPECT_EQ(50, avail);
+ buf->wcommit(50);
+ buf->wbuf(50, avail);
+ EXPECT_EQ(0, avail);
+ buf->wcommit(0);
+ buf->clear();
+ buf->wbuf(5, avail);
+ EXPECT_GE(avail, 5);
+ buf->wcommit(5);
+ buf->rbuf(0, avail);
+ EXPECT_EQ(5, avail);
+ buf->rcommit(3);
+ buf->wbuf(3, avail);
+ EXPECT_GE(avail, 3);
+}
+
+TEST(buffer, fixed_want) {
+ auto buf = Buffer::fixed(10);
+
+ size_t avail;
+ auto* wptr = buf->wbuf(10, avail);
+ ASSERT_EQ(10, avail);
+ std::fill_n(wptr, 10, 'a');
+ buf->wcommit(10);
+
+ auto* rptr = buf->rbuf(0, avail);
+ ASSERT_EQ(10, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ('a', rptr[i]) << i;
+ buf->rcommit(5);
+
+ wptr = buf->wbuf(5, avail);
+ ASSERT_EQ(5, avail);
+ std::fill_n(wptr, 5, 'b');
+ buf->wcommit(5);
+
+ buf->rbuf(0, avail);
+ ASSERT_EQ(5, avail);
+
+ buf->rbuf(10, avail);
+ ASSERT_EQ(10, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ(i < 5 ? 'a' : 'b', rptr[i]) << i;
+ buf->rcommit(10);
+
+ EXPECT_TRUE(buf->empty());
+
+ wptr = buf->wbuf(10, avail);
+ ASSERT_EQ(10, avail);
+ std::fill_n(wptr, 10, 'a');
+ buf->wcommit(10);
+
+ buf->rcommit(5);
+
+ wptr = buf->wbuf(3, avail);
+ ASSERT_EQ(5, avail);
+ std::fill_n(wptr, 3, 'b');
+ buf->wcommit(3);
+
+ buf->rbuf(0, avail);
+ ASSERT_EQ(5, avail);
+
+ buf->rbuf(10, avail);
+ ASSERT_EQ(8, avail);
+ for (size_t i = 0; i < avail; ++i)
+ EXPECT_EQ(i < 5 ? 'a' : 'b', rptr[i]) << i;
+ buf->rcommit(8);
+}
+
+TEST(buffer, read) {
+ auto buf = Buffer::fixed(10);
+ size_t avail;
+ auto* wptr = buf->wbuf(10, avail);
+ ASSERT_EQ(10, avail);
+ std::fill_n(wptr, 5, 'a');
+ std::fill_n(wptr + 5, 5, 'b');
+ buf->wcommit(10);
+
+ char tmp[5];
+ size_t got = Buffer::read(buf.get(), tmp, 0);
+ EXPECT_EQ(0, got);
+ got = Buffer::read(buf.get(), tmp, 5);
+ ASSERT_EQ(5, got);
+ for (size_t i = 0; i < 5; ++i)
+ EXPECT_EQ('a', tmp[i]) << i;
+
+ got = Buffer::read(buf.get(), tmp, 3);
+ ASSERT_EQ(3, got);
+ got = Buffer::read(buf.get(), tmp + 3, 10);
+ ASSERT_EQ(2, got);
+ for (size_t i = 0; i < 5; ++i)
+ EXPECT_EQ('b', tmp[i]) << i;
+
+ got = Buffer::read(buf.get(), tmp, 5);
+ EXPECT_EQ(0, got);
+}
+
+TEST(buffer, write) {
+ char tmp[10];
+ std::fill_n(tmp, 5, 'a');
+ std::fill_n(tmp + 5, 5, 'b');
+
+ auto buf = Buffer::growing(5, 10);
+ auto got = Buffer::write(buf.get(), tmp, 0);
+ EXPECT_EQ(0, got);
+ got = Buffer::write(buf.get(), tmp, 2);
+ EXPECT_EQ(2, got);
+ got = Buffer::write(buf.get(), tmp + 2, 5);
+ EXPECT_EQ(5, got);
+ got = Buffer::write(buf.get(), tmp + 7, 10);
+ EXPECT_EQ(3, got);
+
+ char tmp2[10];
+ got = Buffer::read(buf.get(), tmp2, 10);
+ EXPECT_EQ(10, got);
+
+ for (size_t i = 0; i < 10; ++i)
+ EXPECT_EQ(tmp[i], tmp2[i]) << i;
+}
+
+TEST(buffer, growing_full) {
+ char tmp[10];
+ std::fill_n(tmp, 10, 'x');
+ auto buf = Buffer::growing(5, 10);
+ Buffer::write(buf.get(), tmp, 10);
+ EXPECT_TRUE(buf->full());
+ char tmp2[1];
+ auto got = Buffer::read(buf.get(), tmp2, 1);
+ EXPECT_EQ('x', tmp2[0]);
+ EXPECT_EQ(1, got);
+ EXPECT_FALSE(buf->full());
+}
+
+TEST(buffer, null) {
+ auto buf = Buffer::null();
+ EXPECT_FALSE(buf->full());
+ EXPECT_TRUE(buf->empty());
+ size_t avail;
+ buf->rbuf(1, avail);
+ EXPECT_EQ(0, avail);
+ auto* ptr = buf->wbuf(65536, avail);
+ ASSERT_GE(avail, 1);
+ ptr[0] = 'x';
+ buf->wcommit(1);
+ EXPECT_TRUE(buf->empty());
+ buf->rbuf(1, avail);
+ EXPECT_EQ(0, avail);
+ buf->rcommit(0);
+ buf->clear();
+}
diff --git a/test/test_config.cc b/test/test_config.cc
new file mode 100644
index 0000000..d9ddf3a
--- /dev/null
+++ b/test/test_config.cc
@@ -0,0 +1,167 @@
+#include "common.hh"
+
+#include "config.hh"
+#include "file_test.hh"
+#include "logger.hh"
+#include "unique_fd.hh"
+
+#include <gtest/gtest.h>
+#include <string_view>
+
+namespace {
+
+class ConfigTest : public FileTest {
+protected:
+ ConfigTest()
+ : logger_(Logger::create_null()) {}
+
+ Logger* logger() const {
+ return logger_.get();
+ }
+
+private:
+ std::unique_ptr<Logger> logger_;
+};
+
+} // namespace
+
+TEST(Config, empty) {
+ auto cfg = Config::create_empty();
+ EXPECT_EQ("bar", cfg->get("foo", std::string_view("bar")));
+ EXPECT_STREQ("bar", cfg->get("foo", "bar"));
+ EXPECT_EQ(1000, cfg->get("foo", 1000));
+ EXPECT_EQ(0, cfg->get_size("foo", 0));
+ EXPECT_EQ(0.0, cfg->get_duration("foo", 0.0));
+ EXPECT_TRUE(cfg->get_path("foo", "").empty());
+ EXPECT_EQ("bar", cfg->get_path("foo", "bar"));
+}
+
+TEST_F(ConfigTest, empty_file) {
+ write("");
+ auto cfg = Config::create(logger(), path());
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ("bar", cfg->get("foo", std::string_view("bar")));
+ EXPECT_STREQ("bar", cfg->get("foo", "bar"));
+ EXPECT_EQ(1000, cfg->get("foo", 1000));
+ EXPECT_EQ(0, cfg->get_size("foo", 0));
+ EXPECT_TRUE(cfg->get_path("foo", "").empty());
+}
+
+TEST_F(ConfigTest, sanity) {
+ write("# comment\n"
+ "str = bar\n"
+ "uint = 1000\n"
+ "size = 1M\n"
+ "path = /root\n");
+ auto cfg = Config::create(logger(), path());
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ("bar", cfg->get("str", std::string_view()));
+ EXPECT_STREQ("bar", cfg->get("str", ""));
+ EXPECT_EQ(1000, cfg->get("uint", static_cast<uint64_t>(0)));
+ EXPECT_EQ(1024 * 1024, cfg->get_size("size", 0));
+ EXPECT_EQ("/root", cfg->get_path("path", ""));
+}
+
+TEST_F(ConfigTest, missing_equal) {
+ write("bad file\n");
+ auto cfg = Config::create(logger(), path());
+ EXPECT_FALSE(cfg);
+}
+
+TEST_F(ConfigTest, missing_key) {
+ write("= arg\n");
+ auto cfg = Config::create(logger(), path());
+ EXPECT_FALSE(cfg);
+}
+
+TEST_F(ConfigTest, sizes) {
+ write("bytes1 = 12\n"
+ "bytes2 = 12b\n"
+ "bytes3 = 12B\n"
+ "kilo1 = 1.2k\n"
+ "kilo2 = 1.2K\n"
+ "kilo3 = 1.2Kb\n"
+ "mega1 = 1.2m\n"
+ "mega2 = 1.2M\n"
+ "mega3 = 1.2MB\n"
+ "giga1 = .2g\n"
+ "giga2 = .2G\n"
+ "terra1 = 2.t\n"
+ "terra2 = 2.T\n"
+ "unknown = 4X\n"
+ "nan = X\n");
+ auto cfg = Config::create(logger(), path());
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(12, cfg->get_size("bytes1", 0));
+ EXPECT_EQ(12, cfg->get_size("bytes2", 0));
+ EXPECT_EQ(12, cfg->get_size("bytes3", 0));
+ EXPECT_EQ(1228, cfg->get_size("kilo1", 0));
+ EXPECT_EQ(1228, cfg->get_size("kilo2", 0));
+ EXPECT_EQ(1228, cfg->get_size("kilo3", 0));
+ EXPECT_EQ(1258291, cfg->get_size("mega1", 0));
+ EXPECT_EQ(1258291, cfg->get_size("mega2", 0));
+ EXPECT_EQ(1258291, cfg->get_size("mega3", 0));
+ EXPECT_EQ(static_cast<uint64_t>(214748364), cfg->get_size("giga1", 0));
+ EXPECT_EQ(static_cast<uint64_t>(214748364), cfg->get_size("giga2", 0));
+ EXPECT_EQ(static_cast<uint64_t>(2) * 1024 * 1024 * 1024 * 1024,
+ cfg->get_size("terra1", 0));
+ EXPECT_EQ(static_cast<uint64_t>(2) * 1024 * 1024 * 1024 * 1024,
+ cfg->get_size("terra2", 0));
+ EXPECT_FALSE(cfg->get_size("unknown", 0).has_value());
+ EXPECT_FALSE(cfg->get_size("nan", 0).has_value());
+}
+
+TEST_F(ConfigTest, durations) {
+ write("seconds1 = 1.2\n"
+ "seconds2 = 1.2s\n"
+ "seconds3 = 1.2S\n"
+ "min1 = .5m\n"
+ "min2 = .5M\n"
+ "hour1 = 12.1h\n"
+ "hour2 = 12.1H\n"
+ "milli1 = 100ms\n"
+ "milli2 = 100MS\n"
+ "nano1 = 20000ns\n"
+ "nano2 = 20000NS\n"
+ "unknown = 4X\n"
+ "nan = X\n");
+ auto cfg = Config::create(logger(), path());
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(1.2, cfg->get_duration("seconds1", 0));
+ EXPECT_EQ(1.2, cfg->get_duration("seconds2", 0));
+ EXPECT_EQ(1.2, cfg->get_duration("seconds3", 0));
+ EXPECT_EQ(30., cfg->get_duration("min1", 0));
+ EXPECT_EQ(30., cfg->get_duration("min2", 0));
+ EXPECT_EQ(43560., cfg->get_duration("hour1", 0));
+ EXPECT_EQ(43560., cfg->get_duration("hour2", 0));
+ EXPECT_EQ(.1, cfg->get_duration("milli1", 0));
+ EXPECT_EQ(.1, cfg->get_duration("milli2", 0));
+ EXPECT_EQ(.02, cfg->get_duration("nano1", 0));
+ EXPECT_EQ(.02, cfg->get_duration("nano2", 0));
+ EXPECT_FALSE(cfg->get_duration("unknown", 0).has_value());
+ EXPECT_FALSE(cfg->get_duration("nan", 0).has_value());
+}
+
+TEST_F(ConfigTest, uint64) {
+ write("max = 18446744073709551615\n"
+ "hex = 0xffffffffffffffff\n"
+ "negative = -10\n"
+ "suffix = 100X\n"
+ "nan = X\n");
+ auto cfg = Config::create(logger(), path());
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ(0xffffffffffffffff, cfg->get("max", static_cast<uint64_t>(0)));
+ EXPECT_FALSE(cfg->get("hex", static_cast<uint64_t>(0)).has_value());
+ EXPECT_FALSE(cfg->get("negative", static_cast<uint64_t>(0)).has_value());
+ EXPECT_FALSE(cfg->get("suffix", static_cast<uint64_t>(0)).has_value());
+ EXPECT_FALSE(cfg->get("nan", static_cast<uint64_t>(0)).has_value());
+}
+
+TEST_F(ConfigTest, path) {
+ write("absolute = /file\n"
+ "relative = file\n");
+ auto cfg = Config::create(logger(), path());
+ ASSERT_TRUE(cfg);
+ EXPECT_EQ("/file", cfg->get_path("absolute", ""));
+ EXPECT_EQ(path().parent_path() / "file", cfg->get_path("relative", ""));
+}
diff --git a/test/test_date.cc b/test/test_date.cc
new file mode 100644
index 0000000..5fb0d37
--- /dev/null
+++ b/test/test_date.cc
@@ -0,0 +1,101 @@
+#include "common.hh"
+
+#include "date.hh"
+
+#include <gtest/gtest.h>
+
+TEST(date, empty) {
+ Date d;
+ EXPECT_TRUE(d.empty());
+ EXPECT_TRUE(d == d);
+ EXPECT_FALSE(d < d);
+ Date a(0);
+ EXPECT_FALSE(a.empty());
+ EXPECT_FALSE(d == a);
+ EXPECT_TRUE(d < a);
+}
+
+TEST(date, compare) {
+ Date a(0);
+ Date b(1);
+
+ EXPECT_TRUE(a < b);
+ EXPECT_TRUE(a <= b);
+ EXPECT_FALSE(a > b);
+ EXPECT_FALSE(a >= b);
+ EXPECT_FALSE(a == b);
+ EXPECT_TRUE(a != b);
+
+ EXPECT_FALSE(b < a);
+ EXPECT_FALSE(b <= a);
+ EXPECT_TRUE(b > a);
+ EXPECT_TRUE(b >= a);
+ EXPECT_FALSE(b == a);
+ EXPECT_TRUE(b != a);
+
+ b = a;
+
+ EXPECT_FALSE(a < b);
+ EXPECT_TRUE(a <= b);
+ EXPECT_FALSE(a > b);
+ EXPECT_TRUE(a >= b);
+ EXPECT_TRUE(a == b);
+ EXPECT_FALSE(a != b);
+}
+
+TEST(date, from_format) {
+ auto a = Date::from_format("%Y-%m-%d %H:%M:%S",
+ "1970-01-01 00:00:00",
+ false);
+ EXPECT_FALSE(a.empty());
+ EXPECT_EQ(0, a.value());
+
+ auto b = Date::from_format("%Y-%m-%d %H:%M:%S",
+ "1970-01-01 00:99:00",
+ false);
+ EXPECT_TRUE(b.empty());
+
+ auto c = Date::from_format("%Y-%m-%d %H:%M:%S",
+ "",
+ false);
+ EXPECT_TRUE(c.empty());
+}
+
+TEST(date, day) {
+ auto day = Date::from_format("%Y-%m-%d %H:%M:%S",
+ "1982-04-10 00:00:00");
+ EXPECT_FALSE(day.empty());
+ EXPECT_EQ(day, day.day());
+ auto time = Date::from_format("%Y-%m-%d %H:%M:%S",
+ "1982-04-10 14:42:10");
+ EXPECT_FALSE(time.empty());
+ EXPECT_NE(day, time);
+ EXPECT_EQ(day, time.day());
+
+ Date empty;
+ EXPECT_TRUE(empty.day().empty());
+}
+
+TEST(date, to_format) {
+ auto a = Date::from_format("%Y-%m-%d %H:%M:%S",
+ "1970-01-01 00:00:00",
+ false);
+ EXPECT_EQ("1970-01-01 00:00:00",
+ a.to_format("%Y-%m-%d %H:%M:%S",
+ false));
+ EXPECT_EQ("", a.to_format(""));
+
+ Date b;
+ EXPECT_EQ("", b.to_format("%Y-%m-%d %H:%M:%S"));
+
+ auto c = Date::from_format("%Y-%m-%d %H:%M:%S",
+ "1982-04-10 14:42:10");
+ EXPECT_EQ("1982-04-10 14:42:10",
+ c.to_format("%Y-%m-%d %H:%M:%S"));
+
+ EXPECT_EQ("1982-04-10 14:42:10" "1982-04-10 14:42:10" "1982-04-10 14:42:10"
+ "1982-04-10 14:42:10" "1982-04-10 14:42:10" "1982-04-10 14:42:10",
+ c.to_format("%Y-%m-%d %H:%M:%S" "%Y-%m-%d %H:%M:%S"
+ "%Y-%m-%d %H:%M:%S" "%Y-%m-%d %H:%M:%S"
+ "%Y-%m-%d %H:%M:%S" "%Y-%m-%d %H:%M:%S"));
+}
diff --git a/test/test_document.cc b/test/test_document.cc
new file mode 100644
index 0000000..352a14c
--- /dev/null
+++ b/test/test_document.cc
@@ -0,0 +1,71 @@
+#include "common.hh"
+
+#include "document.hh"
+#include "hash_method.hh"
+#include "mock_transport.hh"
+#include "str_buffer.hh"
+
+#include <gtest/gtest.h>
+
+TEST(document, sanity) {
+ auto doc = Document::create("title");
+
+ doc->add_style("style.css");
+ doc->body()->add("body");
+
+ MockTransport transport;
+ MockResponse response;
+
+ std::string content = "<html><head><title>title</title>"
+ "<link href=\"style.css\" rel=\"stylesheet\"/>"
+ "</head><body>body</body></html>";
+
+ auto hash = HashMethod::sha256();
+ hash->update(content.data(), content.size());
+ auto tag = "\"" + hash->finish() + "\"";
+
+ EXPECT_CALL(
+ transport,
+ create_data_proxy(
+ 200,
+ content))
+ .WillOnce(testing::Return(&response));
+
+ EXPECT_CALL(response, add_header("Content-Type", "text/html; charset=utf-8"));
+ EXPECT_CALL(response, add_header("ETag", tag));
+
+ doc->build(&transport).release();
+}
+
+TEST(document, script) {
+ auto doc = Document::create("");
+
+ doc->add_script("foo.js");
+ auto script = Tag::create("script");
+ script->add("alert(\"<foo>\");");
+ doc->add_script(std::move(script));
+
+ MockTransport transport;
+ MockResponse response;
+
+ std::string content = "<html><head><title/>"
+ "<script src=\"foo.js\" type=\"text/javascript\"></script>"
+ "<script type=\"text/javascript\">alert(\"<foo>\");</script>"
+ "</head><body/></html>";
+
+ auto hash = HashMethod::sha256();
+ hash->update(content.data(), content.size());
+ auto tag = "\"" + hash->finish() + "\"";
+
+ EXPECT_CALL(
+ transport,
+ create_data_proxy(
+ 200,
+ content))
+ .WillOnce(testing::Return(&response));
+
+ EXPECT_CALL(response, add_header("Content-Type", "text/html; charset=utf-8"));
+ EXPECT_CALL(response, add_header("ETag", tag));
+
+ doc->build(&transport).release();
+}
diff --git a/test/test_fcgi_protocol.cc b/test/test_fcgi_protocol.cc
new file mode 100644
index 0000000..b72b6f3
--- /dev/null
+++ b/test/test_fcgi_protocol.cc
@@ -0,0 +1,678 @@
+#include "common.hh"
+
+#include "fcgi_protocol.hh"
+#include "str_buffer.hh"
+
+#include <gtest/gtest.h>
+
+namespace {
+
+constexpr const auto kSanity = std::string_view(
+ "\1" // version = 1
+ "\1" // begin_request = 1
+ "\0\1" // request_id = 1
+ "\x10\xfe" // content_length = 4350
+ "\xf0" // padding_length = 240
+ "\0", // reserved = 0
+ 8);
+
+constexpr const auto kMax = std::string_view(
+ "\1" // version = 1
+ "\xa" // get_values_result = 10
+ "\xff\xff" // request_id = 0xffff
+ "\xff\xff" // content_length = 0xffff
+ "\xff" // padding_length = 0xff
+ "\0", // reserved = 0
+ 8);
+
+constexpr const auto kBeginRequest = std::string_view(
+ "\1" // version = 1
+ "\1" // begin_request = 1
+ "\0\1" // request_id = 1
+ "\0\x8" // content_length = 8
+ "\0" // padding_length = 0
+ "\0" // reserved = 0
+ "\0\1" // role = 1
+ "\1" // flags = 1
+ "\0\0\0\0\0", // reserved
+ 16);
+
+constexpr const auto kPairShortShort = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\0" // request id = 0
+ "\0\x5" // content length = 5
+ "\x3" // padding length = 3
+ "\0" // reserved
+ "\x3" // name length = 3
+ "\0" // value length = 0
+ "foo" // name
+ "" // value
+ "\0\0\0", // padding
+ 16);
+
+#define LONG_NAME \
+ "0123456789012345678901234567890123456789012345678901234567890123456789" \
+ "0123456789012345678901234567890123456789012345678901234567"
+
+#define LONG_VALUE \
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+constexpr const auto kPairShortLong = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\0" // request id = 0
+ "\1\x88" // content length = 392
+ "\0" // padding length = 0
+ "\0" // reserved
+ "\x3" // name length = 3
+ "\x80\0\1\x80" // value length = 384
+ "foo" // name
+ LONG_VALUE
+ "", // padding
+ 400);
+
+constexpr const auto kPairLongShort = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\0" // request id = 0
+ "\0\x85" // content length = 133
+ "\x3" // padding length = 3
+ "\0" // reserved
+ "\x80\0\0\x80" // name length = 128
+ "\0" // value length = 0
+ LONG_NAME
+ "" // value
+ "\0\0\0", // padding
+ 144);
+
+constexpr const auto kPairLongLong = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\0" // request id = 0
+ "\2\x8" // content length = 520
+ "\0" // padding length = 0
+ "\0" // reserved
+ "\x80\0\0\x80" // name length = 128
+ "\x80\0\1\x80" // value length = 384
+ LONG_NAME
+ LONG_VALUE
+ "", // padding
+ 528);
+
+constexpr const auto kStream1 = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\1" // request id = 1
+ "\0\x8" // content length = 8
+ "\0" // padding length = 0
+ "\0" // reserved
+ "\x3" // name length = 3
+ "\0" // value length = 0
+ "foo" // name
+ "" // value
+ "\x3" // name length = 3
+ "\x3" // value length = 3
+ "b", // name[0..1]
+ 16);
+
+constexpr const auto kStream2 = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\1" // request id = 1
+ "\0\x5" // content length = 5
+ "\x3" // padding length = 3
+ "\0" // reserved
+ "ar" // name[2..3]
+ "zum" // value
+ "\0\0\0", // padding
+ 16);
+
+constexpr const auto kStream3 = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\1" // request id = 1
+ "\0\x5" // content length = 5
+ "\x3" // padding length = 3
+ "\0" // reserved
+ "\0" // name length = 0
+ "\x3" // value length = 3
+ "" // name
+ "aaa" // value
+ "\0\0\0", // padding
+ 16);
+
+constexpr const auto kStream4 = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\1" // request id = 1
+ "\0\0" // content length = 0
+ "\0" // padding length = 0
+ "\0" // reserved
+ "", // padding
+ 8);
+
+constexpr const auto kPairInvalid = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\0" // request id = 0
+ "\0\x8" // content length = 8
+ "\0" // padding length = 0
+ "\0" // reserved
+ "\x3" // name length = 3
+ "\x10" // value length = 16
+ "foo" // name
+ "bar" // value (13 bytes to short)
+ "", // padding
+ 16);
+
+constexpr const auto kPairInvalidWithPadding = std::string_view(
+ "\1" // version = 1
+ "\x4" // Params
+ "\0\0" // request id = 0
+ "\0\x6" // content length = 6
+ "\x2" // padding length = 2
+ "\0" // reserved
+ "\x2" // name length = 2
+ "\x8" // value length = 8
+ "aa" // name
+ "bb" // value (13 bytes to short)
+ "\0\0", // padding
+ 16);
+
+} // namespace
+
+TEST(fcgi_protocol, empty) {
+ auto buf = make_strbuffer(std::string_view());
+ EXPECT_FALSE(fcgi::Record::parse(buf.get()));
+}
+
+TEST(fcgi_protocol, incomplete) {
+ for (size_t i = 1; i < kSanity.size(); ++i) {
+ auto buf = make_strbuffer(kSanity.substr(0, i));
+ EXPECT_FALSE(fcgi::Record::parse(buf.get()));
+ }
+}
+
+TEST(fcgi_protocol, bad) {
+ auto buf = make_strbuffer(std::string_view(
+ "\xff\xff\xff\xff\xff\xff\xff\xff"));
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ EXPECT_FALSE(rec->good());
+}
+
+TEST(fcgi_protocol, unknown) {
+ auto buf = make_strbuffer(std::string_view(
+ "\1\xff\xff\xff\xff\xff\xff\xff"));
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ EXPECT_TRUE(rec->good());
+ EXPECT_EQ(0xff, rec->type());
+}
+
+TEST(fcgi_protocol, sanity) {
+ auto buf = make_strbuffer(kSanity);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ EXPECT_TRUE(rec->good());
+ EXPECT_EQ(fcgi::RecordType::BeginRequest, rec->type());
+ EXPECT_EQ(1, rec->request_id());
+ EXPECT_EQ(4350, rec->content_length());
+ EXPECT_EQ(240, rec->padding_length());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, max) {
+ auto buf = make_strbuffer(kMax);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ EXPECT_TRUE(rec->good());
+ EXPECT_EQ(fcgi::RecordType::GetValuesResult, rec->type());
+ EXPECT_EQ(0xffff, rec->request_id());
+ EXPECT_EQ(0xffff, rec->content_length());
+ EXPECT_EQ(0xff, rec->padding_length());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, builder_sanity) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::BeginRequest,
+ 1,
+ 4350,
+ 240);
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ(kSanity, *str);
+ EXPECT_EQ(kSanity.size() + 4350 + 240, builder->size());
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ EXPECT_TRUE(rec->good());
+ EXPECT_EQ(fcgi::RecordType::BeginRequest, rec->type());
+ EXPECT_EQ(1, rec->request_id());
+ EXPECT_EQ(4350, rec->content_length());
+ EXPECT_EQ(240, rec->padding_length());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, builder_with_body) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::BeginRequest,
+ 1,
+ 6,
+ 2);
+ EXPECT_TRUE(builder->build(buf.get()));
+ Buffer::write(buf.get(), "foobar", 6);
+ EXPECT_TRUE(builder->padding(buf.get()));
+
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ EXPECT_TRUE(rec->good());
+ EXPECT_EQ(fcgi::RecordType::BeginRequest, rec->type());
+ EXPECT_EQ(1, rec->request_id());
+ EXPECT_EQ(6, rec->content_length());
+ EXPECT_EQ(2, rec->padding_length());
+
+ char tmp[7];
+ std::fill_n(tmp, sizeof(tmp), 0);
+ EXPECT_EQ(6, Buffer::read(buf.get(), tmp, 6));
+ EXPECT_STREQ("foobar", tmp);
+
+ std::fill_n(tmp, sizeof(tmp), 0);
+ EXPECT_EQ(2, Buffer::read(buf.get(), tmp, 2));
+
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, builder_max) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::GetValuesResult,
+ 0xffff,
+ 0xffff,
+ 0xff);
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ(kMax, *str);
+ EXPECT_EQ(kMax.size() + 0xffff + 0xff, builder->size());
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ EXPECT_TRUE(rec->good());
+ EXPECT_EQ(fcgi::RecordType::GetValuesResult, rec->type());
+ EXPECT_EQ(0xffff, rec->request_id());
+ EXPECT_EQ(0xffff, rec->content_length());
+ EXPECT_EQ(0xff, rec->padding_length());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, builder_incomplete) {
+ auto buf = Buffer::fixed(7);
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::BeginRequest,
+ 1,
+ 0);
+ EXPECT_FALSE(builder->build(buf.get()));
+ EXPECT_EQ(8, builder->size());
+}
+
+TEST(fcgi_protocol, builder_unknown_type) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = fcgi::RecordBuilder::create_unknown_type(0xff);
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ(std::string_view("\1\xb\0\0\0\x8\0\0" "\xff\0\0\0\0\0\0\0", 16),
+ *str);
+ EXPECT_EQ(16, builder->size());
+}
+
+TEST(fcgi_protocol, builder_begin_request) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = fcgi::RecordBuilder::create_begin_request(
+ 1, fcgi::Role::Responder, 0);
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ(std::string_view("\1\1\0\1\0\x8\0\0" "\0\1\0\0\0\0\0\0", 16), *str);
+ EXPECT_EQ(16, builder->size());
+}
+
+TEST(fcgi_protocol, builder_end_request) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = fcgi::RecordBuilder::create_end_request(
+ 1, 0xbeef, fcgi::ProtocolStatus::RequestComplete);
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ(std::string_view("\1\3\0\1\0\x8\0\0" "\0\0\xbe\xef\0\0\0\0", 16),
+ *str);
+ EXPECT_EQ(16, builder->size());
+}
+
+TEST(fcgi_protocol, pair_short_short) {
+ auto buf = make_strbuffer(kPairShortShort);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_single(rec.get());
+ EXPECT_FALSE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ("foo", pair->name());
+ EXPECT_EQ("", pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_TRUE(stream->end_of_stream());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, pair_short_short_incomplete) {
+ for (size_t i = 8; i < kPairShortShort.size() - 1; ++i) {
+ auto buf = make_strbuffer(kPairShortShort.substr(0, i));
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_single(rec.get());
+ EXPECT_FALSE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ EXPECT_FALSE(pair);
+ }
+}
+
+TEST(fcgi_protocol, pair_short_long) {
+ auto buf = make_strbuffer(kPairShortLong);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_single(rec.get());
+ EXPECT_FALSE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ("foo", pair->name());
+ EXPECT_EQ(LONG_VALUE, pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_TRUE(stream->end_of_stream());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, pair_long_short) {
+ auto buf = make_strbuffer(kPairLongShort);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_single(rec.get());
+ EXPECT_FALSE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ(LONG_NAME, pair->name());
+ EXPECT_EQ("", pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_TRUE(stream->end_of_stream());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, pair_long_long) {
+ auto buf = make_strbuffer(kPairLongLong);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_single(rec.get());
+ EXPECT_FALSE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ(LONG_NAME, pair->name());
+ EXPECT_EQ(LONG_VALUE, pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_TRUE(stream->end_of_stream());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, pair_stream) {
+ auto str = std::make_shared<std::string>(kStream1);
+ auto buf = make_strbuffer(str);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_stream(rec.get());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ("foo", pair->name());
+ EXPECT_EQ("", pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+
+ str->append(kStream2);
+ rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ stream->add(rec.get());
+ ASSERT_TRUE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ("bar", pair->name());
+ EXPECT_EQ("zum", pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+
+ str->append(kStream3);
+ rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ stream->add(rec.get());
+ ASSERT_TRUE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ("", pair->name());
+ EXPECT_EQ("aaa", pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+
+ str->append(kStream4);
+ rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ stream->add(rec.get());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_TRUE(stream->end_of_stream());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, pair_stream_incomplete) {
+ for (size_t i = 8; i < kStream2.size() - 1; ++i) {
+ auto str = std::make_shared<std::string>(kStream1);
+ auto buf = make_strbuffer(str);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_stream(rec.get());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_TRUE(pair->good());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+
+ str->append(kStream2.substr(0, i));
+ rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ stream->add(rec.get());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ }
+}
+
+TEST(fcgi_protocol, pair_stream_all_avail) {
+ auto buf = make_strbuffer(std::string(kStream1) +
+ std::string(kStream2) +
+ std::string(kStream3) +
+ std::string(kStream4));
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_stream(rec.get());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ("foo", pair->name());
+ EXPECT_EQ("", pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+
+ rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ stream->add(rec.get());
+ ASSERT_TRUE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ("bar", pair->name());
+ EXPECT_EQ("zum", pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+
+ rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ stream->add(rec.get());
+ ASSERT_TRUE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(pair->good());
+ EXPECT_EQ("", pair->name());
+ EXPECT_EQ("aaa", pair->value());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+
+ rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ stream->add(rec.get());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_TRUE(stream->end_of_stream());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, pair_invalid) {
+ auto buf = make_strbuffer(kPairInvalid);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_single(rec.get());
+ EXPECT_FALSE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_FALSE(pair->good());
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, pair_invalid_with_padding) {
+ auto buf = make_strbuffer(kPairInvalidWithPadding);
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_single(rec.get());
+ EXPECT_FALSE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_FALSE(pair->good());
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_FALSE(stream->end_of_stream());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, pair_stream_invalid) {
+ auto buf = make_strbuffer(std::string(kPairShortShort) +
+ std::string(kPairInvalid) +
+ std::string(kStream4));
+ auto rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ auto stream = fcgi::RecordStream::create_stream(rec.get());
+ auto pair = fcgi::Pair::start(stream.get(), buf.get());
+ ASSERT_TRUE(pair);
+ EXPECT_TRUE(pair->good());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ stream->add(rec.get());
+ EXPECT_FALSE(pair->next(stream.get(), buf.get()));
+ rec = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(rec);
+ stream->add(rec.get());
+ EXPECT_TRUE(pair->next(stream.get(), buf.get()));
+ EXPECT_FALSE(pair->good());
+}
+
+TEST(fcgi_protocol, pair_builder) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = fcgi::PairBuilder::create();
+ builder->add("foo", "");
+ EXPECT_EQ(5, builder->size());
+ builder->add("foo", LONG_VALUE);
+ EXPECT_EQ(397, builder->size());
+ builder->add(LONG_NAME, "");
+ EXPECT_EQ(530, builder->size());
+ builder->add(LONG_NAME, LONG_VALUE);
+ EXPECT_EQ(1050, builder->size());
+ ASSERT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ(std::string_view(
+ // ShortShort
+ "\x3" // name length = 3
+ "\0" // value length = 0
+ "foo" // name
+ "" // value
+
+ // ShortLong
+ "\x3" // name length = 3
+ "\x80\0\1\x80" // value length = 384
+ "foo" // name
+ LONG_VALUE
+
+ // LongShort
+ "\x80\0\0\x80" // name length = 128
+ "\0" // value length = 0
+ LONG_NAME
+ "" // value
+
+ // LongLong
+ "\x80\0\0\x80" // name length = 128
+ "\x80\0\1\x80" // value length = 384
+ LONG_NAME
+ LONG_VALUE,
+ 1050), *str);
+}
+
+TEST(fcgi_protocol, begin_request_body) {
+ auto buf = make_strbuffer(kBeginRequest);
+ auto req = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(req);
+ EXPECT_TRUE(req->good());
+ EXPECT_EQ(fcgi::RecordType::BeginRequest, req->type());
+ auto body = fcgi::BeginRequestBody::parse(req.get(), buf.get());
+ ASSERT_TRUE(body);
+ EXPECT_TRUE(body->good());
+ EXPECT_EQ(1, body->role());
+ EXPECT_EQ(1, body->flags());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(fcgi_protocol, begin_request_body_incomplete) {
+ for (size_t i = 8; i < kBeginRequest.size() - 1; ++i) {
+ auto buf = make_strbuffer(kBeginRequest.substr(0, i));
+ auto req = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(req);
+ auto body = fcgi::BeginRequestBody::parse(req.get(), buf.get());
+ EXPECT_FALSE(body);
+ }
+}
+
+TEST(fcgi_protocol, empty_stream) {
+ auto buf = make_strbuffer(kStream4);
+ auto req = fcgi::Record::parse(buf.get());
+ ASSERT_TRUE(req);
+ auto stream = fcgi::RecordStream::create_stream(req.get());
+ EXPECT_TRUE(stream->end_of_record());
+ EXPECT_TRUE(stream->end_of_stream());
+}
diff --git a/test/test_geo_json.cc b/test/test_geo_json.cc
new file mode 100644
index 0000000..e466e75
--- /dev/null
+++ b/test/test_geo_json.cc
@@ -0,0 +1,123 @@
+#include "common.hh"
+
+#include "file_test.hh"
+#include "geo_json.hh"
+#include "logger.hh"
+
+#include <gtest/gtest.h>
+
+namespace {
+
+class GeoJsonTest : public FileTest {
+public:
+ std::unique_ptr<GeoJson> load(std::string_view data) {
+ write(data);
+ close();
+ return GeoJson::create(logger_, path());
+ }
+
+private:
+ std::shared_ptr<Logger> logger_{Logger::create_null()};
+};
+
+} // namespace
+
+TEST_F(GeoJsonTest, empty) {
+ auto geo_json = load("");
+
+ auto opt = geo_json->get_data(0.0, 0.0, "prop0");
+ EXPECT_FALSE(opt.has_value());
+}
+
+TEST_F(GeoJsonTest, sanity) {
+ auto geo_json = load(
+ "{"
+ " \"type\": \"FeatureCollection\","
+ " \"features\": ["
+ " {"
+ " \"type\": \"Feature\","
+ " \"geometry\": {"
+ " \"type\": \"Polygon\","
+ " \"coordinates\": ["
+ " ["
+ " [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],"
+ " [100.0, 1.0], [100.0, 0.0]"
+ " ]"
+ " ]"
+ " },"
+ " \"properties\": {"
+ " \"prop0\": \"value0\","
+ " \"prop1\": { \"this\": \"that\" }"
+ " }"
+ " }"
+ " ]"
+ "}");
+
+ auto opt = geo_json->get_data(0.0, 0.0, "prop0");
+ EXPECT_FALSE(opt.has_value());
+
+ opt = geo_json->get_data(0.5, 100.5, "prop0");
+ EXPECT_TRUE(opt.has_value());
+ if (opt.has_value()) {
+ EXPECT_EQ("value0", opt.value());
+ }
+
+ opt = geo_json->get_data(0.0, 100.0, "prop0");
+ EXPECT_TRUE(opt.has_value());
+ if (opt.has_value()) {
+ EXPECT_EQ("value0", opt.value());
+ }
+
+ opt = geo_json->get_data(0.5, 100.5, "prop1");
+ EXPECT_FALSE(opt.has_value());
+}
+
+TEST_F(GeoJsonTest, hole) {
+ auto geo_json = load(
+ "{"
+ " \"type\": \"FeatureCollection\","
+ " \"features\": ["
+ " {"
+ " \"type\": \"Feature\","
+ " \"geometry\": {"
+ " \"type\": \"Polygon\","
+ " \"coordinates\": ["
+ " ["
+ " [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],"
+ " [100.0, 1.0], [100.0, 0.0]"
+ " ],"
+ " ["
+ " [100.25, 0.25], [100.75, 0.25], [100.75, 0.75],"
+ " [100.25, 0.75], [100.25, 0.25]"
+ " ]"
+ " ]"
+ " },"
+ " \"properties\": {"
+ " \"prop0\": \"value0\""
+ " }"
+ " }"
+ " ]"
+ "}");
+
+ auto opt = geo_json->get_data(0.5, 100.5, "prop0");
+ EXPECT_FALSE(opt.has_value());
+
+ opt = geo_json->get_data(0.0, 100.0, "prop0");
+ EXPECT_TRUE(opt.has_value());
+ if (opt.has_value()) {
+ EXPECT_EQ("value0", opt.value());
+ }
+
+ opt = geo_json->get_data(0.1, 100.20, "prop0");
+ EXPECT_TRUE(opt.has_value());
+ if (opt.has_value()) {
+ EXPECT_EQ("value0", opt.value());
+ }
+}
+
+TEST_F(GeoJsonTest, bad) {
+ auto geo_json = load(std::string(1000, '{'));
+
+ auto opt = geo_json->get_data(0.0, 0.0, "prop0");
+ EXPECT_FALSE(opt.has_value());
+}
diff --git a/test/test_hash_method.cc b/test/test_hash_method.cc
new file mode 100644
index 0000000..d0c9d20
--- /dev/null
+++ b/test/test_hash_method.cc
@@ -0,0 +1,19 @@
+#include "common.hh"
+
+#include "hash_method.hh"
+
+#include <gtest/gtest.h>
+
+TEST(hash_method, sha256) {
+ auto hash = HashMethod::sha256();
+ EXPECT_EQ("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ hash->finish());
+
+ hash->update("foobar", 6);
+ EXPECT_EQ("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2",
+ hash->finish());
+
+ hash->update("foo", 3);
+ EXPECT_EQ("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
+ hash->finish());
+}
diff --git a/test/test_hasher.cc b/test/test_hasher.cc
new file mode 100644
index 0000000..181b25b
--- /dev/null
+++ b/test/test_hasher.cc
@@ -0,0 +1,82 @@
+#include "common.hh"
+
+#include "file_test.hh"
+#include "hasher.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include "task_runner.hh"
+
+#include <gtest/gtest.h>
+
+namespace {
+
+class HasherTest : public FileTest {
+public:
+ void SetUp() override {
+ FileTest::SetUp();
+
+ hasher_ = Hasher::create(logger_, runner_, 1);
+ }
+
+ void wait() {
+ looper_->run(logger_.get());
+ }
+
+ void TearDown() override {
+ hasher_.reset();
+ std::error_code err;
+ std::filesystem::remove(path(), err);
+ }
+
+ Hasher* hasher() const {
+ return hasher_.get();
+ }
+
+ void quit() {
+ looper_->quit();
+ }
+
+private:
+ std::shared_ptr<Logger> logger_ = Logger::create_null();
+ std::shared_ptr<Looper> looper_ = Looper::create();
+ std::shared_ptr<TaskRunner> runner_ = TaskRunner::create(looper_);
+ std::unique_ptr<Hasher> hasher_;
+};
+
+} // namespace
+
+TEST_F(HasherTest, sanity) {
+ write("foobar");
+ close();
+ hasher()->hash(path(), [this](std::string hash, uint64_t size) {
+ EXPECT_EQ(
+ "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2",
+ hash);
+ EXPECT_EQ(6, size);
+ quit();
+ });
+ wait();
+}
+
+TEST_F(HasherTest, empty) {
+ write("");
+ close();
+ hasher()->hash(path(), [this](std::string hash, uint64_t size) {
+ EXPECT_EQ(
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ hash);
+ EXPECT_EQ(0, size);
+ quit();
+ });
+ wait();
+}
+
+TEST_F(HasherTest, non_existent) {
+ hasher()->hash(path() / "non_existent",
+ [this](std::string hash, uint64_t size) {
+ EXPECT_EQ("", hash);
+ EXPECT_EQ(0, size);
+ quit();
+ });
+ wait();
+}
diff --git a/test/test_htmlutil.cc b/test/test_htmlutil.cc
new file mode 100644
index 0000000..629e646
--- /dev/null
+++ b/test/test_htmlutil.cc
@@ -0,0 +1,26 @@
+#include "common.hh"
+
+#include "htmlutil.hh"
+
+#include <gtest/gtest.h>
+
+TEST(htmlutil, escape_body) {
+ EXPECT_EQ("", html::escape(""));
+ EXPECT_EQ("foo", html::escape("foo"));
+ EXPECT_EQ("&lt;foo&gt;", html::escape("<foo>"));
+ EXPECT_EQ("foo &amp; bar", html::escape("foo & bar"));
+ EXPECT_EQ("\"&amp;lt;\" vs '&amp;gt;'",
+ html::escape("\"&lt;\" vs '&gt;'"));
+ EXPECT_EQ("&lt;&lt;&lt;", html::escape("<<<"));
+}
+
+TEST(htmlutil, escape_attribute) {
+ EXPECT_EQ("", html::escape("", html::EscapeTarget::ATTRIBUTE));
+ EXPECT_EQ("foo", html::escape("foo", html::EscapeTarget::ATTRIBUTE));
+ EXPECT_EQ("&lt;foo&gt;", html::escape("<foo>", html::EscapeTarget::ATTRIBUTE));
+ EXPECT_EQ("foo &amp; bar", html::escape("foo & bar",
+ html::EscapeTarget::ATTRIBUTE));
+ EXPECT_EQ("&quot;&amp;lt;&quot; vs &apos;&amp;gt;&apos;",
+ html::escape("\"&lt;\" vs '&gt;'", html::EscapeTarget::ATTRIBUTE));
+ EXPECT_EQ("&lt;&lt;&lt;", html::escape("<<<", html::EscapeTarget::ATTRIBUTE));
+}
diff --git a/test/test_http_protocol.cc b/test/test_http_protocol.cc
new file mode 100644
index 0000000..9fe1c45
--- /dev/null
+++ b/test/test_http_protocol.cc
@@ -0,0 +1,470 @@
+#include "common.hh"
+
+#include "http_protocol.hh"
+#include "str_buffer.hh"
+
+#include <gtest/gtest.h>
+
+TEST(http_protocol, standard_message) {
+ EXPECT_EQ("", http_standard_message(199));
+ EXPECT_EQ("Continue", http_standard_message(100));
+ EXPECT_EQ("Switching Protocols", http_standard_message(101));
+ EXPECT_EQ("OK", http_standard_message(200));
+ EXPECT_EQ("Created", http_standard_message(201));
+ EXPECT_EQ("Accepted", http_standard_message(202));
+ EXPECT_EQ("Non-Authorative Information", http_standard_message(203));
+ EXPECT_EQ("No Content", http_standard_message(204));
+ EXPECT_EQ("Reset Content", http_standard_message(205));
+ EXPECT_EQ("Partial Content", http_standard_message(206));
+ EXPECT_EQ("Multiple Choices", http_standard_message(300));
+ EXPECT_EQ("Moved Permanently", http_standard_message(301));
+ EXPECT_EQ("Found", http_standard_message(302));
+ EXPECT_EQ("See Other", http_standard_message(303));
+ EXPECT_EQ("Not Modified", http_standard_message(304));
+ EXPECT_EQ("Use Proxy", http_standard_message(305));
+ EXPECT_EQ("Temporary Redirect", http_standard_message(307));
+ EXPECT_EQ("Bad Request", http_standard_message(400));
+ EXPECT_EQ("Unauthorized", http_standard_message(401));
+ EXPECT_EQ("Payment Required", http_standard_message(402));
+ EXPECT_EQ("Forbidden", http_standard_message(403));
+ EXPECT_EQ("Not Found", http_standard_message(404));
+ EXPECT_EQ("Method Not Allowed", http_standard_message(405));
+ EXPECT_EQ("Not Acceptable", http_standard_message(406));
+ EXPECT_EQ("Proxy Authentication Required", http_standard_message(407));
+ EXPECT_EQ("Request Timeout", http_standard_message(408));
+ EXPECT_EQ("Conflict", http_standard_message(409));
+ EXPECT_EQ("Gone", http_standard_message(410));
+ EXPECT_EQ("Length Required", http_standard_message(411));
+ EXPECT_EQ("Precondition Failed", http_standard_message(412));
+ EXPECT_EQ("Request Entity Too Large", http_standard_message(413));
+ EXPECT_EQ("Request-URI Too Long", http_standard_message(414));
+ EXPECT_EQ("Unsupported Media Type", http_standard_message(415));
+ EXPECT_EQ("Requested Range Not Satisfiable", http_standard_message(416));
+ EXPECT_EQ("Expectation Failed", http_standard_message(417));
+ EXPECT_EQ("Internal Server Error", http_standard_message(500));
+ EXPECT_EQ("Not Implemented", http_standard_message(501));
+ EXPECT_EQ("Bad Gateway", http_standard_message(502));
+ EXPECT_EQ("Service Unavailable", http_standard_message(503));
+ EXPECT_EQ("Gateway Timeout", http_standard_message(504));
+ EXPECT_EQ("HTTP Version Not Supported", http_standard_message(505));
+}
+
+TEST(http_protocol, date) {
+ EXPECT_EQ("Thu, 01 Jan 1970 00:00:00 GMT", http_date(0));
+}
+
+TEST(http_protocol, parse_request_empty) {
+ auto buf = make_strbuffer(std::string());
+ EXPECT_EQ(nullptr, HttpRequest::parse(buf.get()));
+}
+
+TEST(http_protocol, parse_request_partial_multiline_header) {
+ auto str = std::string_view(
+ "GET / HTTP/1.1\r\n"
+ " example.org\r\n"
+ "\r\n");
+ auto buf = make_strbuffer(str);
+ auto req = HttpRequest::parse(buf.get());
+ ASSERT_TRUE(req);
+ EXPECT_FALSE(req->good());
+}
+
+TEST(http_protocol, parse_request_simple) {
+ auto str = std::string_view(
+ "GET / HTTP/1.1\r\n"
+ "Host: example.org\r\n"
+ "\r\n");
+ // Verify that partial requests fail
+ for (size_t i = 1; i < str.size() - 1; ++i) {
+ auto buf = make_strbuffer(str.substr(0, i));
+ EXPECT_EQ(nullptr, HttpRequest::parse(buf.get()));
+ }
+ auto buf = make_strbuffer(str);
+ auto req = HttpRequest::parse(buf.get());
+ ASSERT_TRUE(req);
+ EXPECT_TRUE(req->good());
+ EXPECT_EQ("GET", req->method());
+ EXPECT_EQ("/", req->url());
+ EXPECT_EQ("HTTP", req->proto());
+ EXPECT_EQ(1, req->proto_version().major);
+ EXPECT_EQ(1, req->proto_version().minor);
+ EXPECT_EQ("example.org", req->first_header("host"));
+ EXPECT_EQ(str.size(), req->size());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(http_protocol, parse_request_multiple) {
+ auto post = std::string_view(
+ "POST /foo HTTP/1.1\r\n"
+ "Host: example.org\r\n"
+ "Content-Length: 3\r\n"
+ "Content-Type: text/plain; charset=utf-8\r\n"
+ "\r\n"
+ "bar");
+ auto get = std::string_view(
+ "GET /bar HTTP/1.0\r\n"
+ "\r\n");
+ auto buf = make_strbuffer(std::string(post) + std::string(get));
+ auto req = HttpRequest::parse(buf.get());
+ ASSERT_TRUE(req);
+ EXPECT_EQ("POST", req->method());
+ EXPECT_EQ("/foo", req->url());
+ EXPECT_EQ("HTTP", req->proto());
+ EXPECT_EQ(1, req->proto_version().major);
+ EXPECT_EQ(1, req->proto_version().minor);
+ EXPECT_EQ("example.org", req->first_header("host"));
+ EXPECT_EQ("3", req->first_header("content-length"));
+ EXPECT_EQ("text/plain; charset=utf-8", req->first_header("content-type"));
+ auto token_it = req->header_tokens("content-type");
+ EXPECT_TRUE(token_it->valid());
+ if (token_it->valid()) {
+ EXPECT_EQ("text", token_it->token());
+ token_it->next();
+ EXPECT_FALSE(token_it->valid());
+ }
+ EXPECT_EQ(post.size() - 3, req->size());
+ char tmp[3];
+ RoBuffer::read(buf.get(), tmp, 3);
+ EXPECT_EQ("bar", std::string_view(tmp, 3));
+ req = HttpRequest::parse(buf.get());
+ ASSERT_TRUE(req);
+ EXPECT_TRUE(req->good());
+ EXPECT_EQ("GET", req->method());
+ EXPECT_EQ("/bar", req->url());
+ EXPECT_EQ("HTTP", req->proto());
+ EXPECT_EQ(1, req->proto_version().major);
+ EXPECT_EQ(0, req->proto_version().minor);
+ EXPECT_EQ("", req->first_header("host"));
+ EXPECT_EQ(get.size(), req->size());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(http_protocol, parse_request_multiline_header) {
+ auto str = std::string_view(
+ "GET / HTTP/1.1\r\n"
+ "Accept: image/jpeg, image/png\r\n"
+ " image/gif\r\n"
+ "Accept: image/x-webp\r\n"
+ "\r\n");
+ auto buf = make_strbuffer(str);
+ auto req = HttpRequest::parse(buf.get());
+ ASSERT_TRUE(req);
+ EXPECT_TRUE(req->good());
+ EXPECT_EQ("image/jpeg, image/png,image/gif",
+ req->first_header("accept"));
+ std::string values;
+ for (auto it = req->header("accept"); it->valid(); it->next()) {
+ values.push_back('|');
+ values.append(it->value());
+ }
+ EXPECT_EQ("|image/jpeg, image/png,image/gif|image/x-webp", values);
+ EXPECT_EQ(str.size(), req->size());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(http_protocol, parse_request_multiline_set_cookie_header) {
+ // Set-Cookie can't be joined, so the below really shouldn't
+ // ever be sent. Could treat it as invalid, could pretend
+ // Set-Cookie was set three times, or, as current implmenetation does,
+ // ignore the multiline line.
+ auto str = std::string_view(
+ "GET / HTTP/1.1\r\n"
+ "Set-Cookie: a=1\r\n"
+ " b=2\r\n"
+ "Set-Cookie: c=3\r\n"
+ "\r\n");
+ auto buf = make_strbuffer(str);
+ auto req = HttpRequest::parse(buf.get());
+ ASSERT_TRUE(req);
+ EXPECT_TRUE(req->good());
+ EXPECT_EQ("a=1", req->first_header("set-cookie"));
+ std::string values;
+ for (auto it = req->header("set-cookie"); it->valid(); it->next()) {
+ values.push_back('|');
+ values.append(it->value());
+ }
+ EXPECT_EQ("|a=1|c=3", values);
+ EXPECT_EQ(str.size(), req->size());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(http_protocol, parse_request_tokens) {
+ // Note that this mainly shows that you shouldn't use header_tokens() for
+ // if-none-match... but it's a header that contains a lot of quoted strings.
+ auto str = std::string_view(
+ "GET / HTTP/1.1\r\n"
+ "If-None-Match: *, W/\"foo\", \"bar\"\r\n"
+ "\r\n");
+ auto buf = make_strbuffer(str);
+ auto req = HttpRequest::parse(buf.get());
+ ASSERT_TRUE(req);
+ EXPECT_TRUE(req->good());
+ EXPECT_EQ("*, W/\"foo\", \"bar\"", req->first_header("if-none-match"));
+ std::string tokens;
+ for (auto it = req->header_tokens("if-none-match"); it->valid(); it->next()) {
+ tokens.push_back('|');
+ tokens.append(it->token());
+ }
+ EXPECT_EQ("|*|W", tokens);
+ EXPECT_EQ(str.size(), req->size());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(http_protocol, request_builder_simple) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = HttpRequestBuilder::create("GET", "/", "HTTP", Version(1, 1));
+ builder->add_header("Host", "example.org");
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ("GET / HTTP/1.1\r\n"
+ "Host: example.org\r\n"
+ "\r\n", *str);
+ EXPECT_EQ(str->size(), builder->size());
+}
+
+TEST(http_protocol, request_builder_post) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = HttpRequestBuilder::create("POST", "/foo", "HTTP",
+ Version(1, 1));
+ builder->add_header("Host", "example.org");
+ builder->add_header("Content-Length", "3");
+ builder->add_header("Content-Type", "text/plain; charset=utf-8");
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ("POST /foo HTTP/1.1\r\n"
+ "Host: example.org\r\n"
+ "Content-Length: 3\r\n"
+ "Content-Type: text/plain; charset=utf-8\r\n"
+ "\r\n", *str);
+ EXPECT_EQ(str->size(), builder->size());
+}
+
+TEST(http_protocol, request_builder_10) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = HttpRequestBuilder::create("GET", "/bar", "HTTP",
+ Version(1, 0));
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ("GET /bar HTTP/1.0\r\n"
+ "\r\n", *str);
+ EXPECT_EQ(str->size(), builder->size());
+}
+
+TEST(http_protocol, request_builder_multiline_header) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = HttpRequestBuilder::create("GET", "/", "HTTP", Version(1, 1));
+ builder->add_header("Accept", "image/jpeg, image/png");
+ builder->add_header("", "image/gif");
+ builder->add_header("Accept", "image/x-webp");
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ("GET / HTTP/1.1\r\n"
+ "Accept: image/jpeg, image/png\r\n"
+ " image/gif\r\n"
+ "Accept: image/x-webp\r\n"
+ "\r\n", *str);
+ EXPECT_EQ(str->size(), builder->size());
+}
+
+TEST(http_protocol, parse_response_empty) {
+ auto buf = make_strbuffer(std::string());
+ EXPECT_EQ(nullptr, HttpResponse::parse(buf.get()));
+}
+
+TEST(http_protocol, parse_response_simple) {
+ auto str = std::string_view(
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Length: 3\r\n"
+ "Content-Type: text/plain; charset=utf-8\r\n"
+ "Connection: close\r\n"
+ "\r\n"
+ "foo");
+ // Verify partial parses fail
+ for (size_t i = 1; i < str.size() - 4; ++i) {
+ auto buf = make_strbuffer(str.substr(0, i));
+ EXPECT_EQ(nullptr, HttpResponse::parse(buf.get()));
+ }
+ auto buf = make_strbuffer(str);
+ auto resp = HttpResponse::parse(buf.get());
+ ASSERT_TRUE(resp);
+ EXPECT_TRUE(resp->good());
+ EXPECT_EQ("HTTP", resp->proto());
+ EXPECT_EQ(1, resp->proto_version().major);
+ EXPECT_EQ(1, resp->proto_version().minor);
+ EXPECT_EQ(200, resp->status_code());
+ EXPECT_EQ("OK", resp->status_message());
+ EXPECT_EQ("3", resp->first_header("content-length"));
+ EXPECT_EQ("text/plain; charset=utf-8", resp->first_header("content-type"));
+ EXPECT_EQ("close", resp->first_header("connection"));
+ EXPECT_EQ(str.size() - 3, resp->size());
+ {
+ char tmp[3];
+ RoBuffer::read(buf.get(), tmp, 3);
+ EXPECT_EQ("foo", std::string_view(tmp, 3));
+ }
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(http_protocol, parse_response_multiple) {
+ auto redirect = std::string_view(
+ "HTTP/1.1 302 Redirecting for fun\r\n"
+ "Location: /foo\r\n"
+ "\r\n");
+ auto result = std::string_view(
+ "HTTP/1.0 204 No Content\r\n"
+ "Connection: close\r\n"
+ "\r\n");
+ auto buf = make_strbuffer(std::string(redirect) + std::string(result));
+ auto resp = HttpResponse::parse(buf.get());
+ ASSERT_TRUE(resp);
+ EXPECT_TRUE(resp->good());
+ EXPECT_EQ("HTTP", resp->proto());
+ EXPECT_EQ(1, resp->proto_version().major);
+ EXPECT_EQ(1, resp->proto_version().minor);
+ EXPECT_EQ(302, resp->status_code());
+ EXPECT_EQ("Redirecting for fun", resp->status_message());
+ EXPECT_EQ("/foo", resp->first_header("location"));
+ EXPECT_EQ(redirect.size(), resp->size());
+ resp = HttpResponse::parse(buf.get());
+ ASSERT_TRUE(resp);
+ EXPECT_EQ("HTTP", resp->proto());
+ EXPECT_EQ(1, resp->proto_version().major);
+ EXPECT_EQ(0, resp->proto_version().minor);
+ EXPECT_EQ(204, resp->status_code());
+ EXPECT_EQ("No Content", resp->status_message());
+ auto token_it = resp->header_tokens("connection");
+ EXPECT_TRUE(token_it->valid());
+ if (token_it->valid()) {
+ EXPECT_EQ("close", token_it->token());
+ token_it->next();
+ EXPECT_FALSE(token_it->valid());
+ }
+ EXPECT_EQ(result.size(), resp->size());
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(http_protocol, parse_response_simple_only_newline) {
+ auto str = std::string_view(
+ "HTTP/1.1 200 OK\n"
+ "Content-Length: 3\n"
+ "Content-Type: text/plain; charset=utf-8\n"
+ "Connection: close\n"
+ "\n"
+ "foo");
+ for (size_t i = 1; i < str.size() - 4; ++i) {
+ auto buf = make_strbuffer(str.substr(0, i));
+ EXPECT_EQ(nullptr, HttpResponse::parse(buf.get()));
+ }
+ auto buf = make_strbuffer(str);
+ auto resp = HttpResponse::parse(buf.get());
+ ASSERT_TRUE(resp);
+ EXPECT_TRUE(resp->good());
+ EXPECT_EQ("HTTP", resp->proto());
+ EXPECT_EQ(1, resp->proto_version().major);
+ EXPECT_EQ(1, resp->proto_version().minor);
+ EXPECT_EQ(200, resp->status_code());
+ EXPECT_EQ("OK", resp->status_message());
+ EXPECT_EQ("3", resp->first_header("content-length"));
+ EXPECT_EQ("text/plain; charset=utf-8", resp->first_header("content-type"));
+ EXPECT_EQ("close", resp->first_header("connection"));
+ EXPECT_EQ(str.size() - 3, resp->size());
+ {
+ char tmp[3];
+ RoBuffer::read(buf.get(), tmp, 3);
+ EXPECT_EQ("foo", std::string_view(tmp, 3));
+ }
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(http_protocol, parse_response_simple_only_linebreak) {
+ auto str = std::string_view(
+ "HTTP/1.1 200 OK\r"
+ "Content-Length: 3\r"
+ "Content-Type: text/plain; charset=utf-8\r"
+ "Connection: close\r"
+ "\r"
+ "foo");
+ for (size_t i = 1; i < str.size() - 4; ++i) {
+ auto buf = make_strbuffer(str.substr(0, i));
+ EXPECT_EQ(nullptr, HttpResponse::parse(buf.get()));
+ }
+ auto buf = make_strbuffer(str);
+ auto resp = HttpResponse::parse(buf.get());
+ ASSERT_TRUE(resp);
+ EXPECT_TRUE(resp->good());
+ EXPECT_EQ("HTTP", resp->proto());
+ EXPECT_EQ(1, resp->proto_version().major);
+ EXPECT_EQ(1, resp->proto_version().minor);
+ EXPECT_EQ(200, resp->status_code());
+ EXPECT_EQ("OK", resp->status_message());
+ EXPECT_EQ("3", resp->first_header("content-length"));
+ EXPECT_EQ("text/plain; charset=utf-8", resp->first_header("content-type"));
+ EXPECT_EQ("close", resp->first_header("connection"));
+ EXPECT_EQ(str.size() - 3, resp->size());
+ {
+ char tmp[3];
+ RoBuffer::read(buf.get(), tmp, 3);
+ EXPECT_EQ("foo", std::string_view(tmp, 3));
+ }
+ EXPECT_TRUE(buf->empty());
+}
+
+TEST(http_protocol, response_builder_simple) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+
+ auto builder = HttpResponseBuilder::create("HTTP", Version(1, 1), 200, "OK");
+ builder->add_header("Content-Length", "3");
+ builder->add_header("Content-Type", "text/plain; charset=utf-8");
+ builder->add_header("Connection", "close");
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ("HTTP/1.1 200 OK\r\n"
+ "Content-Length: 3\r\n"
+ "Content-Type: text/plain; charset=utf-8\r\n"
+ "Connection: close\r\n"
+ "\r\n", *str);
+ EXPECT_EQ(str->size(), builder->size());
+}
+
+TEST(http_protocol, response_builder_redirect) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = HttpResponseBuilder::create("HTTP", Version(1, 1),
+ 302, "Redirecting for fun");
+ builder->add_header("Location", "/foo");
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ("HTTP/1.1 302 Redirecting for fun\r\n"
+ "Location: /foo\r\n"
+ "\r\n", *str);
+ EXPECT_EQ(str->size(), builder->size());
+}
+
+TEST(http_protocol, response_builder_no_content) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+ auto builder = HttpResponseBuilder::create("HTTP", Version(1, 0),
+ 204, "No Content");
+ builder->add_header("Connection", "close");
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ("HTTP/1.0 204 No Content\r\n"
+ "Connection: close\r\n"
+ "\r\n", *str);
+ EXPECT_EQ(str->size(), builder->size());
+}
+
+TEST(http_protocol, response_cgi_builder_simple) {
+ auto str = std::make_shared<std::string>();
+ auto buf = make_strbuffer(str);
+
+ auto builder = CgiResponseBuilder::create(200);
+ builder->add_header("Content-Length", "3");
+ builder->add_header("Content-Type", "text/plain; charset=utf-8");
+ builder->add_header("Connection", "close");
+ EXPECT_TRUE(builder->build(buf.get()));
+ EXPECT_EQ("Status: 200\r\n"
+ "Content-Length: 3\r\n"
+ "Content-Type: text/plain; charset=utf-8\r\n"
+ "Connection: close\r\n"
+ "\r\n", *str);
+ EXPECT_EQ(str->size(), builder->size());
+}
diff --git a/test/test_image.cc b/test/test_image.cc
new file mode 100644
index 0000000..c0d116f
--- /dev/null
+++ b/test/test_image.cc
@@ -0,0 +1,206 @@
+#include "common.hh"
+
+#include "file_test.hh"
+#include "image.hh"
+
+#include <gtest/gtest.h>
+
+namespace {
+
+static const uint8_t kSmallJpeg[] = {
+ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,
+ 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c,
+ 0x01, 0x2c, 0x00, 0x00, 0xff, 0xe2, 0x02, 0xb0,
+ 0x49, 0x43, 0x43, 0x5f, 0x50, 0x52, 0x4f, 0x46,
+ 0x49, 0x4c, 0x45, 0x00, 0x01, 0x01, 0x00, 0x00,
+ 0x02, 0xa0, 0x6c, 0x63, 0x6d, 0x73, 0x04, 0x30,
+ 0x00, 0x00, 0x6d, 0x6e, 0x74, 0x72, 0x52, 0x47,
+ 0x42, 0x20, 0x58, 0x59, 0x5a, 0x20, 0x07, 0xe5,
+ 0x00, 0x0b, 0x00, 0x0a, 0x00, 0x16, 0x00, 0x02,
+ 0x00, 0x2e, 0x61, 0x63, 0x73, 0x70, 0x41, 0x50,
+ 0x50, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xf6, 0xd6, 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0xd3, 0x2d, 0x6c, 0x63,
+ 0x6d, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x0d, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00,
+ 0x01, 0x20, 0x00, 0x00, 0x00, 0x40, 0x63, 0x70,
+ 0x72, 0x74, 0x00, 0x00, 0x01, 0x60, 0x00, 0x00,
+ 0x00, 0x36, 0x77, 0x74, 0x70, 0x74, 0x00, 0x00,
+ 0x01, 0x98, 0x00, 0x00, 0x00, 0x14, 0x63, 0x68,
+ 0x61, 0x64, 0x00, 0x00, 0x01, 0xac, 0x00, 0x00,
+ 0x00, 0x2c, 0x72, 0x58, 0x59, 0x5a, 0x00, 0x00,
+ 0x01, 0xd8, 0x00, 0x00, 0x00, 0x14, 0x62, 0x58,
+ 0x59, 0x5a, 0x00, 0x00, 0x01, 0xec, 0x00, 0x00,
+ 0x00, 0x14, 0x67, 0x58, 0x59, 0x5a, 0x00, 0x00,
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x14, 0x72, 0x54,
+ 0x52, 0x43, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00,
+ 0x00, 0x20, 0x67, 0x54, 0x52, 0x43, 0x00, 0x00,
+ 0x02, 0x14, 0x00, 0x00, 0x00, 0x20, 0x62, 0x54,
+ 0x52, 0x43, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00,
+ 0x00, 0x20, 0x63, 0x68, 0x72, 0x6d, 0x00, 0x00,
+ 0x02, 0x34, 0x00, 0x00, 0x00, 0x24, 0x64, 0x6d,
+ 0x6e, 0x64, 0x00, 0x00, 0x02, 0x58, 0x00, 0x00,
+ 0x00, 0x24, 0x64, 0x6d, 0x64, 0x64, 0x00, 0x00,
+ 0x02, 0x7c, 0x00, 0x00, 0x00, 0x24, 0x6d, 0x6c,
+ 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e,
+ 0x55, 0x53, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00,
+ 0x00, 0x1c, 0x00, 0x47, 0x00, 0x49, 0x00, 0x4d,
+ 0x00, 0x50, 0x00, 0x20, 0x00, 0x62, 0x00, 0x75,
+ 0x00, 0x69, 0x00, 0x6c, 0x00, 0x74, 0x00, 0x2d,
+ 0x00, 0x69, 0x00, 0x6e, 0x00, 0x20, 0x00, 0x73,
+ 0x00, 0x52, 0x00, 0x47, 0x00, 0x42, 0x6d, 0x6c,
+ 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e,
+ 0x55, 0x53, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x00,
+ 0x00, 0x1c, 0x00, 0x50, 0x00, 0x75, 0x00, 0x62,
+ 0x00, 0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x20,
+ 0x00, 0x44, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x61,
+ 0x00, 0x69, 0x00, 0x6e, 0x00, 0x00, 0x58, 0x59,
+ 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xf6, 0xd6, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0xd3, 0x2d, 0x73, 0x66, 0x33, 0x32, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x01, 0x0c, 0x42, 0x00, 0x00,
+ 0x05, 0xde, 0xff, 0xff, 0xf3, 0x25, 0x00, 0x00,
+ 0x07, 0x93, 0x00, 0x00, 0xfd, 0x90, 0xff, 0xff,
+ 0xfb, 0xa1, 0xff, 0xff, 0xfd, 0xa2, 0x00, 0x00,
+ 0x03, 0xdc, 0x00, 0x00, 0xc0, 0x6e, 0x58, 0x59,
+ 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x6f, 0xa0, 0x00, 0x00, 0x38, 0xf5, 0x00, 0x00,
+ 0x03, 0x90, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x24, 0x9f, 0x00, 0x00,
+ 0x0f, 0x84, 0x00, 0x00, 0xb6, 0xc4, 0x58, 0x59,
+ 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x62, 0x97, 0x00, 0x00, 0xb7, 0x87, 0x00, 0x00,
+ 0x18, 0xd9, 0x70, 0x61, 0x72, 0x61, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02,
+ 0x66, 0x66, 0x00, 0x00, 0xf2, 0xa7, 0x00, 0x00,
+ 0x0d, 0x59, 0x00, 0x00, 0x13, 0xd0, 0x00, 0x00,
+ 0x0a, 0x5b, 0x63, 0x68, 0x72, 0x6d, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00,
+ 0xa3, 0xd7, 0x00, 0x00, 0x54, 0x7c, 0x00, 0x00,
+ 0x4c, 0xcd, 0x00, 0x00, 0x99, 0x9a, 0x00, 0x00,
+ 0x26, 0x67, 0x00, 0x00, 0x0f, 0x5c, 0x6d, 0x6c,
+ 0x75, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x65, 0x6e,
+ 0x55, 0x53, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
+ 0x00, 0x1c, 0x00, 0x47, 0x00, 0x49, 0x00, 0x4d,
+ 0x00, 0x50, 0x6d, 0x6c, 0x75, 0x63, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x0c, 0x65, 0x6e, 0x55, 0x53, 0x00, 0x00,
+ 0x00, 0x08, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x73,
+ 0x00, 0x52, 0x00, 0x47, 0x00, 0x42, 0xff, 0xdb,
+ 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02,
+ 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x03,
+ 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05,
+ 0x0a, 0x07, 0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c,
+ 0x0c, 0x0b, 0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12,
+ 0x10, 0x0d, 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10,
+ 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15,
+ 0x0c, 0x0f, 0x17, 0x18, 0x16, 0x14, 0x18, 0x12,
+ 0x14, 0x15, 0x14, 0xff, 0xdb, 0x00, 0x43, 0x01,
+ 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x09, 0x05,
+ 0x05, 0x09, 0x14, 0x0d, 0x0b, 0x0d, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
+ 0xff, 0xc2, 0x00, 0x11, 0x08, 0x00, 0x01, 0x00,
+ 0x01, 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x01,
+ 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x14, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x07, 0xff, 0xc4, 0x00, 0x14, 0x01, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+ 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x10,
+ 0x03, 0x10, 0x00, 0x00, 0x01, 0x55, 0x3f, 0xff,
+ 0xc4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00,
+ 0x08, 0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0x7f,
+ 0xff, 0xc4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda,
+ 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3f, 0x01,
+ 0x7f, 0xff, 0xc4, 0x00, 0x14, 0x11, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+ 0xda, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3f,
+ 0x01, 0x7f, 0xff, 0xc4, 0x00, 0x14, 0x10, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06,
+ 0x3f, 0x02, 0x7f, 0xff, 0xc4, 0x00, 0x14, 0x10,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00,
+ 0x01, 0x3f, 0x21, 0x7f, 0xff, 0xda, 0x00, 0x0c,
+ 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00,
+ 0x00, 0x10, 0x9f, 0xff, 0xc4, 0x00, 0x14, 0x11,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x03, 0x01,
+ 0x01, 0x3f, 0x10, 0x7f, 0xff, 0xc4, 0x00, 0x14,
+ 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x02,
+ 0x01, 0x01, 0x3f, 0x10, 0x7f, 0xff, 0xc4, 0x00,
+ 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01,
+ 0x01, 0x00, 0x01, 0x3f, 0x10, 0x7f, 0xff, 0xd9,
+};
+
+class ImageTest : public FileTest {
+public:
+ void write_small_jpeg() {
+ write(std::string_view(reinterpret_cast<char const*>(kSmallJpeg),
+ sizeof(kSmallJpeg)));
+ close();
+ }
+
+ void write_bad_jpeg() {
+ write(std::string_view(reinterpret_cast<char const*>(kSmallJpeg),
+ sizeof(kSmallJpeg) / 2));
+ close();
+ }
+
+ std::string const& extension() override {
+ static std::string jpeg = ".jpeg";
+ return jpeg;
+ }
+};
+
+} // namespace
+
+TEST_F(ImageTest, small_jpeg) {
+ write_small_jpeg();
+ auto img = Image::load(path());
+ ASSERT_TRUE(img);
+ EXPECT_EQ(1, img->width());
+ EXPECT_EQ(1, img->height());
+ EXPECT_TRUE(img->location().empty());
+ EXPECT_EQ(Rotation::UNKNOWN, img->rotation());
+ EXPECT_TRUE(img->date().empty());
+}
+
+TEST_F(ImageTest, bad_jpeg) {
+ write_bad_jpeg();
+ auto img = Image::load(path());
+ EXPECT_FALSE(img);
+}
+
+TEST_F(ImageTest, non_existant) {
+ auto img = Image::load(path() / "non_existant");
+ EXPECT_FALSE(img);
+}
diff --git a/test/test_jsutil.cc b/test/test_jsutil.cc
new file mode 100644
index 0000000..8bf3140
--- /dev/null
+++ b/test/test_jsutil.cc
@@ -0,0 +1,15 @@
+#include "common.hh"
+
+#include "jsutil.hh"
+
+#include <gtest/gtest.h>
+
+TEST(jsutil, quote) {
+ EXPECT_EQ("\"\"", js::quote(""));
+ EXPECT_EQ("''", js::quote("", js::QuoteChar::SINGLE));
+ EXPECT_EQ("\"foo\"", js::quote("foo"));
+ EXPECT_EQ("\"\\\"foo\\\"\"", js::quote("\"foo\""));
+ EXPECT_EQ("\"\\\\\\\"foo\\\\\\\"\"", js::quote("\\\"foo\\\""));
+ EXPECT_EQ("\"\\0\\n\\r\\v\\t\\b\\f\"", js::quote(
+ std::string_view("\0\n\r\v\t\b\f", 7)));
+}
diff --git a/test/test_mime_types.cc b/test/test_mime_types.cc
new file mode 100644
index 0000000..383d464
--- /dev/null
+++ b/test/test_mime_types.cc
@@ -0,0 +1,12 @@
+#include "common.hh"
+
+#include "mime_types.hh"
+
+#include <gtest/gtest.h>
+
+TEST(mime_types, sanity) {
+ EXPECT_EQ("text/css", mime_types::from_extension("css"));
+ EXPECT_EQ("image/jpeg", mime_types::from_extension("jpeg"));
+ EXPECT_EQ("image/jpeg", mime_types::from_extension("jpg"));
+ EXPECT_EQ("", mime_types::from_extension(""));
+}
diff --git a/test/test_observer_list.cc b/test/test_observer_list.cc
new file mode 100644
index 0000000..705f701
--- /dev/null
+++ b/test/test_observer_list.cc
@@ -0,0 +1,119 @@
+#include "common.hh"
+
+#include "observer_list.hh"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace {
+
+class Observer {
+public:
+ virtual ~Observer() = default;
+ virtual void foo() = 0;
+};
+
+class MockObserver : public Observer {
+public:
+ MOCK_METHOD(void, foo, (), (override));
+};
+
+} // namespace
+
+TEST(observer_list, empty) {
+ ObserverList<Observer> observers;
+ EXPECT_TRUE(observers.empty());
+ auto it = observers.notify();
+ EXPECT_FALSE(it);
+ ++it;
+ EXPECT_FALSE(it);
+}
+
+TEST(observer_list, sanity) {
+ ObserverList<Observer> observers;
+ MockObserver observer;
+ observers.add(&observer);
+ EXPECT_CALL(observer, foo());
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+}
+
+TEST(observer_list, observer_removing_next_observer) {
+ ObserverList<Observer> observers;
+ MockObserver observer1;
+ MockObserver observer2;
+ observers.add(&observer1);
+ observers.add(&observer2);
+ EXPECT_CALL(observer1, foo())
+ .WillOnce([&]() { observers.remove(&observer2); })
+ .WillOnce(testing::Return());
+ EXPECT_CALL(observer2, foo())
+ .Times(0);
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+}
+
+TEST(observer_list, observer_removing_previous_observer) {
+ ObserverList<Observer> observers;
+ MockObserver observer1;
+ MockObserver observer2;
+ observers.add(&observer1);
+ observers.add(&observer2);
+ EXPECT_CALL(observer1, foo());
+ EXPECT_CALL(observer2, foo())
+ .WillOnce([&]() { observers.remove(&observer1); })
+ .WillOnce(testing::Return());
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+}
+
+TEST(observer_list, observer_removing_itself) {
+ ObserverList<Observer> observers;
+ MockObserver observer;
+ observers.add(&observer);
+ EXPECT_CALL(observer, foo())
+ .WillOnce([&]() { observers.remove(&observer); });
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+ EXPECT_TRUE(observers.empty());
+}
+
+TEST(observer_list, observer_adding_observer) {
+ ObserverList<Observer> observers;
+ MockObserver observer1;
+ MockObserver observer2;
+ observers.add(&observer1);
+ EXPECT_CALL(observer1, foo())
+ .WillOnce([&]() { observers.add(&observer2); })
+ .WillOnce(testing::Return());
+ EXPECT_CALL(observer2, foo());
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+}
+
+TEST(observer_list, observer_start_new_notify) {
+ ObserverList<Observer> observers;
+ MockObserver observer1;
+ MockObserver observer2;
+ observers.add(&observer1);
+ observers.add(&observer2);
+ EXPECT_CALL(observer1, foo())
+ .WillOnce([&]() {
+ observers.remove(&observer1);
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+ });
+ EXPECT_CALL(observer2, foo())
+ .Times(2);
+ for (auto it = observers.notify(); it; ++it)
+ it->foo();
+}
diff --git a/test/test_pathutil.cc b/test/test_pathutil.cc
new file mode 100644
index 0000000..4d12e8f
--- /dev/null
+++ b/test/test_pathutil.cc
@@ -0,0 +1,30 @@
+#include "common.hh"
+
+#include "pathutil.hh"
+
+#include <gtest/gtest.h>
+
+TEST(pathutil, cleanup) {
+ EXPECT_EQ("/", path::cleanup(""));
+ EXPECT_EQ("/", path::cleanup("/"));
+ EXPECT_EQ("/", path::cleanup("//"));
+ EXPECT_EQ("/", path::cleanup("///////"));
+ EXPECT_EQ("/", path::cleanup("."));
+ EXPECT_EQ("/", path::cleanup("/."));
+ EXPECT_EQ("/", path::cleanup("/./"));
+ EXPECT_EQ("/", path::cleanup("/././././"));
+ EXPECT_EQ("/", path::cleanup("./"));
+ EXPECT_EQ("/", path::cleanup(".."));
+ EXPECT_EQ("/", path::cleanup("../"));
+ EXPECT_EQ("/", path::cleanup("/../"));
+ EXPECT_EQ("/", path::cleanup("/.."));
+ EXPECT_EQ("/", path::cleanup("/../../../.."));
+ EXPECT_EQ("/foo/", path::cleanup("/../../../../foo/"));
+ EXPECT_EQ("/foo", path::cleanup("/foo"));
+ EXPECT_EQ("/foo/", path::cleanup("/foo/"));
+ EXPECT_EQ("/foo/", path::cleanup("////foo////"));
+ EXPECT_EQ("/foo/", path::cleanup("/./foo/"));
+ EXPECT_EQ("/foo/", path::cleanup("/foo/."));
+ EXPECT_EQ("/", path::cleanup("/foo/.."));
+ EXPECT_EQ("/bar", path::cleanup("/foo/../bar"));
+}
diff --git a/test/test_signal_handler.cc b/test/test_signal_handler.cc
new file mode 100644
index 0000000..511ab3c
--- /dev/null
+++ b/test/test_signal_handler.cc
@@ -0,0 +1,63 @@
+#include "common.hh"
+
+#include "logger.hh"
+#include "looper.hh"
+#include "signal_handler.hh"
+
+#include <gtest/gtest.h>
+
+class SignalHandlerTestFixture :
+ public testing::TestWithParam<std::pair<SignalHandler::Signal, int>> {
+};
+
+TEST_P(SignalHandlerTestFixture, raise_before_loop) {
+ bool called = false;
+ auto logger = Logger::create_null();
+ std::shared_ptr<Looper> looper = Looper::create();
+ auto handler = SignalHandler::create(looper,
+ GetParam().first,
+ [&called, &looper] {
+ called = true;
+ looper->quit();
+ });
+ raise(GetParam().second);
+ looper->run(logger.get());
+ EXPECT_TRUE(called);
+}
+
+TEST_P(SignalHandlerTestFixture, raise_during_loop) {
+ bool called = false;
+ auto logger = Logger::create_null();
+ std::shared_ptr<Looper> looper = Looper::create();
+ auto handler = SignalHandler::create(looper,
+ GetParam().first,
+ [&called, &looper] {
+ called = true;
+ looper->quit();
+ });
+ looper->schedule(0, [] (uint32_t) { raise(GetParam().second); });
+ looper->run(logger.get());
+ EXPECT_TRUE(called);
+}
+
+TEST_P(SignalHandlerTestFixture, no_raise) {
+ bool called = false;
+ auto logger = Logger::create_null();
+ std::shared_ptr<Looper> looper = Looper::create();
+ auto handler = SignalHandler::create(looper,
+ GetParam().first,
+ [&called, &looper] {
+ called = true;
+ looper->quit();
+ });
+ looper->schedule(0, [&looper] (uint32_t) { looper->quit(); });
+ looper->run(logger.get());
+ EXPECT_FALSE(called);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ SignalHandlerTests,
+ SignalHandlerTestFixture,
+ testing::Values(std::make_pair(SignalHandler::Signal::INT, SIGINT),
+ std::make_pair(SignalHandler::Signal::TERM, SIGTERM),
+ std::make_pair(SignalHandler::Signal::HUP, SIGHUP)));
diff --git a/test/test_strutil.cc b/test/test_strutil.cc
new file mode 100644
index 0000000..4b65218
--- /dev/null
+++ b/test/test_strutil.cc
@@ -0,0 +1,219 @@
+#include "common.hh"
+
+#include "strutil.hh"
+
+#include <gtest/gtest.h>
+
+TEST(strutil, parse_uint16) {
+ std::optional<uint16_t> value;
+ value = str::parse_uint16("0");
+ EXPECT_TRUE(value.has_value());
+ if (value.has_value())
+ EXPECT_EQ(0, value.value());
+ value = str::parse_uint16("10");
+ EXPECT_TRUE(value.has_value());
+ if (value.has_value())
+ EXPECT_EQ(10, value.value());
+ value = str::parse_uint16("65535");
+ EXPECT_TRUE(value.has_value());
+ if (value.has_value())
+ EXPECT_EQ(65535, value.value());
+
+ EXPECT_FALSE(str::parse_uint16("-1"));
+ EXPECT_FALSE(str::parse_uint16("0x10"));
+ EXPECT_FALSE(str::parse_uint16("65536"));
+ EXPECT_FALSE(str::parse_uint16(""));
+ EXPECT_FALSE(str::parse_uint16("NaN"));
+}
+
+TEST(strutil, parse_uint32) {
+ std::optional<uint32_t> value;
+ value = str::parse_uint32("0");
+ EXPECT_TRUE(value.has_value());
+ if (value.has_value())
+ EXPECT_EQ(0, value.value());
+ value = str::parse_uint32("10");
+ EXPECT_TRUE(value.has_value());
+ if (value.has_value())
+ EXPECT_EQ(10, value);
+ value = str::parse_uint32("4294967295");
+ EXPECT_TRUE(value.has_value());
+ if (value.has_value())
+ EXPECT_EQ(4294967295ul, value);
+
+ EXPECT_FALSE(str::parse_uint32("-1"));
+ EXPECT_FALSE(str::parse_uint32("0x10"));
+ EXPECT_FALSE(str::parse_uint32("4294967296"));
+ EXPECT_FALSE(str::parse_uint32(""));
+ EXPECT_FALSE(str::parse_uint32("NaN"));
+}
+
+TEST(strutil, parse_uint64) {
+ std::optional<uint64_t> value;
+ value = str::parse_uint64("0");
+ EXPECT_TRUE(value.has_value());
+ if (value.has_value())
+ EXPECT_EQ(0, value.value());
+ value = str::parse_uint64("10");
+ EXPECT_TRUE(value.has_value());
+ if (value.has_value())
+ EXPECT_EQ(10, value.value());
+ value = str::parse_uint64("18446744073709551615");
+ EXPECT_TRUE(value.has_value());
+ if (value.has_value())
+ EXPECT_EQ(18446744073709551615ull, value.value());
+
+ EXPECT_FALSE(str::parse_uint64("-1"));
+ EXPECT_FALSE(str::parse_uint64("0x10"));
+ EXPECT_FALSE(str::parse_uint64("18446744073709551616"));
+ EXPECT_FALSE(str::parse_uint64(""));
+ EXPECT_FALSE(str::parse_uint64("NaN"));
+}
+
+TEST(strutil, split) {
+ auto out = str::split(std::string_view(""));
+ ASSERT_EQ(1, out.size());
+ EXPECT_EQ("", out[0]);
+
+ out = str::split(std::string_view("foo"));
+ ASSERT_EQ(1, out.size());
+ EXPECT_EQ("foo", out[0]);
+
+ out = str::split(std::string_view(" f o o "));
+ ASSERT_EQ(3, out.size());
+ EXPECT_EQ("f", out[0]);
+ EXPECT_EQ("o", out[1]);
+ EXPECT_EQ("o", out[2]);
+
+ out = str::split(std::string_view(" f o o "));
+ ASSERT_EQ(3, out.size());
+ EXPECT_EQ("f", out[0]);
+ EXPECT_EQ("o", out[1]);
+ EXPECT_EQ("o", out[2]);
+
+ out = str::split(std::string_view("abba"), 'b');
+ ASSERT_EQ(2, out.size());
+ EXPECT_EQ("a", out[0]);
+ EXPECT_EQ("a", out[1]);
+}
+
+TEST(strutil, split_str) {
+ auto out = str::split(std::string(""));
+ ASSERT_EQ(1, out.size());
+ EXPECT_EQ("", out[0]);
+
+ out = str::split(std::string("foo"));
+ ASSERT_EQ(1, out.size());
+ EXPECT_EQ("foo", out[0]);
+
+ out = str::split(std::string(" f o o "));
+ ASSERT_EQ(3, out.size());
+ EXPECT_EQ("f", out[0]);
+ EXPECT_EQ("o", out[1]);
+ EXPECT_EQ("o", out[2]);
+
+ out = str::split(std::string(" f o o "));
+ ASSERT_EQ(3, out.size());
+ EXPECT_EQ("f", out[0]);
+ EXPECT_EQ("o", out[1]);
+ EXPECT_EQ("o", out[2]);
+
+ out = str::split(std::string("abba"), 'b');
+ ASSERT_EQ(2, out.size());
+ EXPECT_EQ("a", out[0]);
+ EXPECT_EQ("a", out[1]);
+}
+
+TEST(strutil, trim_str) {
+ EXPECT_EQ("", str::trim(std::string("")));
+ EXPECT_EQ("foo", str::trim(std::string("foo")));
+ EXPECT_EQ("foo", str::trim(std::string("foo ")));
+ EXPECT_EQ("foo", str::trim(std::string(" foo")));
+ EXPECT_EQ("foo", str::trim(std::string(" foo ")));
+ EXPECT_EQ("foo", str::trim(std::string(" foo ")));
+
+ EXPECT_EQ("", str::ltrim(std::string("")));
+ EXPECT_EQ("foo", str::ltrim(std::string("foo")));
+ EXPECT_EQ("foo ", str::ltrim(std::string("foo ")));
+ EXPECT_EQ("foo", str::ltrim(std::string(" foo")));
+ EXPECT_EQ("foo ", str::ltrim(std::string(" foo ")));
+ EXPECT_EQ("foo ", str::ltrim(std::string(" foo ")));
+
+ EXPECT_EQ("", str::rtrim(std::string("")));
+ EXPECT_EQ("foo", str::rtrim(std::string("foo")));
+ EXPECT_EQ("foo", str::rtrim(std::string("foo ")));
+ EXPECT_EQ(" foo", str::rtrim(std::string(" foo")));
+ EXPECT_EQ(" foo", str::rtrim(std::string(" foo ")));
+ EXPECT_EQ(" foo", str::rtrim(std::string(" foo ")));
+}
+
+TEST(strutil, trim_view) {
+ EXPECT_EQ("", str::trim(std::string_view("")));
+ EXPECT_EQ("foo", str::trim(std::string_view("foo")));
+ EXPECT_EQ("foo", str::trim(std::string_view("foo ")));
+ EXPECT_EQ("foo", str::trim(std::string_view(" foo")));
+ EXPECT_EQ("foo", str::trim(std::string_view(" foo ")));
+ EXPECT_EQ("foo", str::trim(std::string_view(" foo ")));
+
+ EXPECT_EQ("", str::ltrim(std::string("")));
+ EXPECT_EQ("foo", str::ltrim(std::string("foo")));
+ EXPECT_EQ("foo ", str::ltrim(std::string("foo ")));
+ EXPECT_EQ("foo", str::ltrim(std::string(" foo")));
+ EXPECT_EQ("foo ", str::ltrim(std::string(" foo ")));
+ EXPECT_EQ("foo ", str::ltrim(std::string(" foo ")));
+
+ EXPECT_EQ("", str::rtrim(std::string("")));
+ EXPECT_EQ("foo", str::rtrim(std::string("foo")));
+ EXPECT_EQ("foo", str::rtrim(std::string("foo ")));
+ EXPECT_EQ(" foo", str::rtrim(std::string(" foo")));
+ EXPECT_EQ(" foo", str::rtrim(std::string(" foo ")));
+ EXPECT_EQ(" foo", str::rtrim(std::string(" foo ")));
+}
+
+TEST(strutil, starts_with) {
+ EXPECT_TRUE(str::starts_with("", ""));
+ EXPECT_TRUE(str::starts_with("foo", ""));
+ EXPECT_TRUE(str::starts_with("foo", "foo"));
+ EXPECT_FALSE(str::starts_with("foo", "foobar"));
+ EXPECT_TRUE(str::starts_with("foo", "f"));
+ EXPECT_FALSE(str::starts_with("foo", "o"));
+ EXPECT_TRUE(str::starts_with("bar", ""));
+ EXPECT_FALSE(str::starts_with("bar", "foo"));
+ EXPECT_FALSE(str::starts_with("bar", "f"));
+ EXPECT_FALSE(str::starts_with("bar", "barfoo"));
+}
+
+TEST(strutil, ends_with) {
+ EXPECT_TRUE(str::ends_with("", ""));
+ EXPECT_TRUE(str::ends_with("foo", ""));
+ EXPECT_TRUE(str::ends_with("foo", "foo"));
+ EXPECT_FALSE(str::ends_with("foo", "barfoo"));
+ EXPECT_TRUE(str::ends_with("foo", "o"));
+ EXPECT_FALSE(str::ends_with("foo", "f"));
+ EXPECT_TRUE(str::ends_with("bar", ""));
+ EXPECT_FALSE(str::ends_with("bar", "foo"));
+ EXPECT_FALSE(str::ends_with("bar", "f"));
+ EXPECT_FALSE(str::ends_with("bar", "barfoo"));
+}
+
+TEST(strutil, join_str) {
+ EXPECT_EQ("", str::join(std::vector<std::string>(), ','));
+ EXPECT_EQ("foo", str::join(std::vector<std::string>({"foo"}), ','));
+ EXPECT_EQ("foo,bar", str::join(std::vector<std::string>({"foo", "bar"}),
+ ','));
+ EXPECT_EQ(",,", str::join(std::vector<std::string>({"", "", ""}),
+ ','));
+ EXPECT_EQ(",foo,", str::join(std::vector<std::string>({",", ","}),
+ "foo"));
+}
+
+TEST(strutil, join_view) {
+ EXPECT_EQ("", str::join(std::vector<std::string_view>(), ','));
+ EXPECT_EQ("foo", str::join(std::vector<std::string_view>({"foo"}), ','));
+ EXPECT_EQ("foo,bar", str::join(std::vector<std::string_view>({"foo", "bar"}),
+ ','));
+ EXPECT_EQ(",,", str::join(std::vector<std::string_view>({"", "", ""}),
+ ','));
+ EXPECT_EQ(",foo,", str::join(std::vector<std::string_view>({",", ","}),
+ "foo"));
+}
diff --git a/test/test_tag.cc b/test/test_tag.cc
new file mode 100644
index 0000000..d65f8d2
--- /dev/null
+++ b/test/test_tag.cc
@@ -0,0 +1,63 @@
+#include "common.hh"
+
+#include "tag.hh"
+
+#include <gtest/gtest.h>
+
+TEST(tag, empty) {
+ auto tag = Tag::create("br");
+ std::string out;
+ tag->render(&out);
+ EXPECT_EQ("<br/>", out);
+}
+
+TEST(tag, text) {
+ auto tag = Tag::create("p");
+ tag->add("Hello <b>World</b>!");
+ std::string out;
+ tag->render(&out);
+ EXPECT_EQ("<p>Hello &lt;b&gt;World&lt;/b&gt;!</p>", out);
+}
+
+TEST(tag, tags_and_text) {
+ auto tag = Tag::create("p");
+ tag->add("Hello ");
+ tag->add_tag("b", "World");
+ tag->add("!");
+ EXPECT_FALSE(tag->empty());
+ std::string out;
+ tag->render(&out);
+ EXPECT_EQ("<p>Hello <b>World</b>!</p>", out);
+ tag->clear_content();
+ EXPECT_TRUE(tag->empty());
+ tag->add("Goodbye");
+ out.clear();
+ tag->render(&out);
+ EXPECT_EQ("<p>Goodbye</p>", out);
+}
+
+TEST(tag, onclick) {
+ auto tag = Tag::create("a");
+ tag->add("Link");
+ tag->attr("href", "http://example.org");
+ tag->attr("onclick", "alert('Hello World');");
+ std::string out;
+ tag->render(&out);
+ EXPECT_EQ("<a href=\"http://example.org\" onclick=\"alert(&apos;Hello World&apos;);\">Link</a>", out);
+}
+
+TEST(tag, script_with_content) {
+ auto tag = Tag::create("script");
+ tag->add("alert('Hello <b>World</b>');");
+ std::string out;
+ tag->render(&out);
+ EXPECT_EQ("<script>alert('Hello <b>World</b>');</script>", out);
+}
+
+TEST(tag, script_with_src) {
+ auto tag = Tag::create("script");
+ tag->attr("src", "/js/helloworld.js");
+ std::string out;
+ tag->render(&out);
+ EXPECT_EQ("<script src=\"/js/helloworld.js\"></script>", out);
+}
diff --git a/test/test_task_runner.cc b/test/test_task_runner.cc
new file mode 100644
index 0000000..d259b6a
--- /dev/null
+++ b/test/test_task_runner.cc
@@ -0,0 +1,174 @@
+#include "common.hh"
+
+#include "logger.hh"
+#include "looper.hh"
+#include "task_runner.hh"
+#include "task_runner_reply.hh"
+
+#include <atomic>
+#include <future>
+#include <gtest/gtest.h>
+#include <thread>
+#include <vector>
+
+namespace {
+
+class TaskRunnerTest : public testing::Test {
+protected:
+ virtual TaskRunner* runner() = 0;
+ virtual std::shared_ptr<TaskRunner> shared_runner() = 0;
+ virtual void run_until_idle() = 0;
+};
+
+class TaskRunnerLooper : public TaskRunnerTest {
+protected:
+ TaskRunnerLooper()
+ : logger_(Logger::create_null()),
+ looper_(Looper::create()),
+ runner_(TaskRunner::create(looper_)) {
+ }
+
+ TaskRunner* runner() override {
+ return runner_.get();
+ }
+
+ std::shared_ptr<TaskRunner> shared_runner() override {
+ return runner_;
+ }
+
+ void run_until_idle() override {
+ runner_->post(std::bind(&Looper::quit, looper_.get()));
+ looper_->run(logger_.get());
+ }
+
+private:
+ std::unique_ptr<Logger> logger_;
+ std::shared_ptr<Looper> looper_;
+ std::shared_ptr<TaskRunner> runner_;
+};
+
+class TaskRunnerThread : public TaskRunnerTest,
+ public testing::WithParamInterface<int> {
+protected:
+ void SetUp() override {
+ runner_ = TaskRunner::create(GetParam());
+ }
+
+ TaskRunner* runner() override {
+ return runner_.get();
+ }
+
+ std::shared_ptr<TaskRunner> shared_runner() override {
+ return runner_;
+ }
+
+ void run_until_idle() override {
+ std::promise<bool> done;
+ auto done_future = done.get_future();
+ runner_->post([&done] { done.set_value(true); });
+ done_future.wait();
+ // Done above makes sure all of the queue has been finished
+ // but we also need to run the destructor for runner_ to make
+ // sure all threads have finished running the callback they have.
+ runner_ = TaskRunner::create(GetParam());
+ }
+
+private:
+ std::shared_ptr<TaskRunner> runner_;
+};
+
+} // namespace
+
+TEST_F(TaskRunnerLooper, sanity) {
+ auto value = std::make_unique<int>(0);
+ auto* value_ptr = value.get();
+ for (int i = 0; i < 100; ++i)
+ runner()->post([value_ptr] { (*value_ptr)++; });
+ run_until_idle();
+ EXPECT_EQ(100, *value);
+}
+
+TEST_F(TaskRunnerLooper, thread) {
+ auto value = std::make_shared<int>(0);
+ auto const main_thread_id = std::this_thread::get_id();
+ auto* tmp = runner();
+ std::vector<std::thread> threads;
+ for (size_t i = 0; i < 10; ++i) {
+ threads.emplace_back([&value, tmp, main_thread_id] {
+ tmp->post([&value, main_thread_id] {
+ if (std::this_thread::get_id() == main_thread_id) {
+ (*value)++;
+ }
+ });
+ });
+ }
+ for (auto& thread : threads) thread.join();
+ run_until_idle();
+ EXPECT_EQ(10, *value);
+}
+
+TEST_F(TaskRunnerLooper, reply) {
+ int result = 0;
+ std::function<int()> callback = [] () -> int {
+ return 10;
+ };
+ std::function<void(int)> reply = [&result] (int value) {
+ result = value;
+ };
+ post_and_reply(
+ runner(),
+ std::move(callback),
+ shared_runner(),
+ std::move(reply));
+ run_until_idle();
+ EXPECT_EQ(10, result);
+}
+
+/*
+TEST_F(TaskRunnerLooper, reply_unique) {
+ int result = 0;
+ std::function<std::unique_ptr<int>()> callback =
+ [] () -> std::unique_ptr<int> {
+ return std::make_unique<int>(10);
+ };
+ std::function<void(std::unique_ptr<int>)> reply =
+ [&result] (std::unique_ptr<int> value) {
+ result = *value;
+ };
+ post_and_reply(
+ runner(),
+ std::move(callback),
+ shared_runner(),
+ std::move(reply));
+ run_until_idle();
+ EXPECT_EQ(10, result);
+}
+*/
+
+TEST_P(TaskRunnerThread, sanity) {
+ std::atomic<int> value(0);
+ for (int i = 0; i < 100; ++i)
+ runner()->post([&value] { value++; });
+ run_until_idle();
+ EXPECT_EQ(100, value);
+}
+
+TEST_P(TaskRunnerThread, thread) {
+ std::mutex mutex;
+ std::set<std::thread::id> threads;
+ for (int i = 0; i < 100; ++i)
+ runner()->post([&threads, &mutex] {
+ bool new_thread;
+ {
+ std::unique_lock<std::mutex> lock(mutex);
+ auto pair = threads.insert(std::this_thread::get_id());
+ new_thread = pair.second;
+ }
+ if (new_thread)
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ });
+ run_until_idle();
+ EXPECT_EQ(GetParam(), threads.size());
+}
+
+INSTANTIATE_TEST_SUITE_P(Threads, TaskRunnerThread, testing::Values(1, 2, 10));
diff --git a/test/test_transport_fcgi.cc b/test/test_transport_fcgi.cc
new file mode 100644
index 0000000..7ef1bde
--- /dev/null
+++ b/test/test_transport_fcgi.cc
@@ -0,0 +1,490 @@
+#include "common.hh"
+
+#include "config.hh"
+#include "fcgi_protocol.hh"
+#include "file_test.hh"
+#include "io.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include "socket_test.hh"
+#include "str_buffer.hh"
+#include "task_runner.hh"
+#include "transport_fastcgi.hh"
+
+#include <gtest/gtest.h>
+#include <map>
+
+namespace {
+
+class TransportFcgiTest : public SocketTest, public Transport::Handler {
+public:
+ ~TransportFcgiTest() override {
+ if (!path_.empty()) {
+ std::error_code err;
+ std::filesystem::remove(path_, err);
+ }
+ }
+
+ void SetUp() override {
+ fd_ = FileTest::create_temp_file(std::string(), &path_);
+
+ auto config = Config::create_empty();
+ auto factory = create_transport_factory_fastcgi();
+ handler_ = Transport::create_default_handler(logger_, this);
+ transport_ = factory->create(logger_, looper(), runner_, logger_.get(),
+ config.get(), handler_.get());
+ }
+
+ void TearDown() override {
+ transport_.reset();
+ handler_.reset();
+ runner_.reset();
+ }
+
+ void write_file(std::string_view content) {
+ ASSERT_TRUE(fd_);
+ auto buffer = make_strbuffer(content);
+ while (!buffer->empty()) {
+ ASSERT_TRUE(io::drain(buffer.get(), fd_.get()));
+ }
+ ASSERT_TRUE(io::close(fd_.release()));
+ }
+
+ std::unique_ptr<Transport::Response> request(
+ Transport* transport, Transport::Request const* request) override {
+ if (request->method() == "GET") {
+ if (request->path() == "/hello_world")
+ return transport->create_ok_data("Hello World!");
+ if (request->path() == "/file")
+ return transport->create_ok_file(path_);
+ }
+ return transport->create_not_found();;
+ }
+
+ Transport* transport() { return transport_.get(); }
+
+ Logger* logger() { return logger_.get(); }
+
+private:
+ std::shared_ptr<Logger> logger_ = Logger::create_null();
+ std::shared_ptr<TaskRunner> runner_ = TaskRunner::create(looper());
+ std::unique_ptr<Transport::Handler> handler_;
+ std::unique_ptr<Transport> transport_;
+ std::filesystem::path path_;
+ unique_fd fd_;
+};
+
+void make_request(SocketTest::Client* cli, uint16_t request_id,
+ std::string const& path, uint8_t flags,
+ size_t step) {
+ switch (step) {
+ case 0: {
+ auto builder = fcgi::RecordBuilder::create_begin_request(
+ request_id, fcgi::Role::Responder, flags);
+ cli->write([&] (Buffer* buf) {
+ ASSERT_TRUE(builder->build(buf));
+ });
+ break;
+ }
+ case 1: {
+ auto pair_builder = fcgi::PairBuilder::create();
+ pair_builder->add("REQUEST_METHOD", "GET");
+ pair_builder->add("REQUEST_URI", path);
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::Params,
+ request_id,
+ pair_builder->size());
+ cli->write([&] (Buffer* buf) {
+ ASSERT_TRUE(builder->build(buf));
+ ASSERT_TRUE(pair_builder->build(buf));
+ ASSERT_TRUE(builder->padding(buf));
+ });
+ break;
+ }
+ case 2: {
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::Params,
+ request_id,
+ std::string());
+ cli->write([&] (Buffer* buf) {
+ ASSERT_TRUE(builder->build(buf));
+ });
+ break;
+ }
+ case 3: {
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::Stdin,
+ request_id,
+ std::string());
+ cli->write([&] (Buffer* buf) {
+ ASSERT_TRUE(builder->build(buf));
+ });
+ break;
+ }
+ default:
+ FAIL();
+ }
+}
+
+void make_request(SocketTest::Client* cli, uint16_t request_id,
+ std::string const& path, uint8_t flags) {
+ for (size_t step = 0; step < 4; ++step) {
+ make_request(cli, request_id, path, flags, step);
+ }
+}
+
+struct Response {
+ std::unique_ptr<fcgi::Record> record;
+ std::unique_ptr<fcgi::RecordStream> stdout_stream;
+ std::string stdout;
+ std::optional<std::string> ended;
+
+ void reset() {
+ record.reset();
+ stdout_stream.reset();
+ stdout.clear();
+ ended.reset();
+ }
+};
+
+void read_response_content(SocketTest::Client* cli, uint16_t request_id,
+ Response* response, bool* need_more) {
+ assert(!response->ended.has_value());
+ while (true) {
+ assert(response->record);
+ ASSERT_EQ(request_id, response->record->request_id());
+ if (response->record->type() == fcgi::RecordType::EndRequest) {
+ if (cli->received().size() <
+ response->record->content_length() +
+ response->record->padding_length()) {
+ ASSERT_FALSE(cli->closed());
+ *need_more = true;
+ return;
+ }
+ EXPECT_EQ(8, response->record->content_length());
+ response->ended = cli->received().substr(
+ 0, response->record->content_length());
+ cli->forget(response->record->content_length() +
+ response->record->padding_length());
+ response->record.reset();
+ *need_more = false;
+ return;
+ }
+ ASSERT_EQ(fcgi::RecordType::Stdout, response->record->type());
+ cli->read([&] (RoBuffer* buf) {
+ size_t avail;
+ auto* ptr = response->stdout_stream->rbuf(buf, 1, avail);
+ response->stdout.append(ptr, avail);
+ response->stdout_stream->rcommit(buf, avail);
+ });
+ if (response->stdout_stream->end_of_record()) {
+ response->record.reset();
+ *need_more = false;
+ return;
+ }
+ }
+}
+
+void read_responses(SocketTest::Client* cli,
+ std::map<uint16_t, Response*>const& responses,
+ bool* need_more) {
+ *need_more = false;
+ auto it = responses.begin();
+ while (it != responses.end()) {
+ if (it->second->record) {
+ read_response_content(cli, it->first, it->second, need_more);
+ if (*need_more)
+ return;
+ } else {
+ ++it;
+ }
+ }
+
+ while (true) {
+ bool all_ended = true;
+ for (auto& pair : responses) {
+ if (!pair.second->ended.has_value()) {
+ all_ended = false;
+ break;
+ }
+ }
+ if (all_ended) {
+ *need_more = false;
+ return;
+ }
+
+ std::unique_ptr<fcgi::Record> record;
+ cli->read([&] (RoBuffer* buf) {
+ record = fcgi::Record::parse(buf);
+ });
+ if (!record) {
+ ASSERT_FALSE(cli->closed());
+ *need_more = true;
+ return;
+ }
+ ASSERT_TRUE(record->good());
+ it = responses.find(record->request_id());
+ if (it == responses.end() || it->second->ended.has_value()) {
+ FAIL();
+ return;
+ }
+ auto* response = it->second;
+ response->record = std::move(record);
+ if (response->record->type() == fcgi::RecordType::Stdout) {
+ if (!response->stdout_stream) {
+ response->stdout_stream =
+ fcgi::RecordStream::create_stream(response->record.get());
+ } else {
+ ASSERT_FALSE(response->stdout_stream->end_of_stream());
+ response->stdout_stream->add(response->record.get());
+ }
+ }
+
+ do {
+ *need_more = false;
+ read_response_content(cli, it->first, response, need_more);
+ if (*need_more)
+ return;
+ } while (response->record);
+ }
+}
+
+void read_response(SocketTest::Client* cli, uint16_t request_id,
+ Response* response, bool* need_more) {
+ std::map<uint16_t, Response*> responses;
+ responses[request_id] = response;
+ read_responses(cli, responses, need_more);
+}
+
+struct GetValuesResponse {
+ std::unique_ptr<fcgi::Record> record;
+ std::unique_ptr<fcgi::RecordStream> stream;
+ std::unique_ptr<fcgi::Pair> pair;
+ std::vector<std::pair<std::string, std::string>> values;
+};
+
+void read_response(SocketTest::Client* cli, GetValuesResponse* response,
+ bool* need_more) {
+ while (true) {
+ if (!response->record) {
+ cli->read([&] (RoBuffer* buf) {
+ response->record = fcgi::Record::parse(buf);
+ });
+ if (!response->record) {
+ ASSERT_FALSE(cli->closed());
+ *need_more = true;
+ return;
+ }
+ ASSERT_TRUE(response->record->good());
+ ASSERT_EQ(fcgi::RecordType::GetValuesResult, response->record->type());
+ if (!response->stream) {
+ response->stream =
+ fcgi::RecordStream::create_single(response->record.get());
+ }
+ if (!response->pair) {
+ cli->read([&] (RoBuffer* buf) {
+ response->pair = fcgi::Pair::start(response->stream.get(), buf);
+ });
+ if (!response->pair) {
+ ASSERT_TRUE(response->stream->end_of_record());
+ response->record.reset();
+ *need_more = true;
+ return;
+ }
+ ASSERT_TRUE(response->pair->good());
+ response->values.emplace_back(response->pair->name(),
+ response->pair->value());
+ }
+ }
+
+ ASSERT_TRUE(response->pair);
+
+ while (true) {
+ bool next;
+ cli->read([&] (RoBuffer* buf) {
+ next = response->pair->next(response->stream.get(), buf);
+ });
+ if (next) {
+ response->values.emplace_back(response->pair->name(),
+ response->pair->value());
+ } else {
+ ASSERT_TRUE(response->pair->good());
+ response->record.reset();
+ if (response->stream->end_of_stream()) {
+ *need_more = false;
+ return;
+ }
+ *need_more = true;
+ return;
+ }
+ }
+ }
+}
+
+} // namespace
+
+TEST_F(TransportFcgiTest, sanity) {
+ auto pair = create_pair();
+ ASSERT_TRUE(pair.first && pair.second);
+ transport()->add_client(std::move(pair.first));
+ auto cli = create_client(std::move(pair.second));
+
+ make_request(cli.get(), 1, "/hello_world", 0);
+
+ Response response;
+
+ while (true) {
+ bool need_more;
+ read_response(cli.get(), 1, &response, &need_more);
+ if (need_more) {
+ cli->wait(logger());
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ ASSERT_TRUE(response.stdout_stream);
+ EXPECT_TRUE(response.stdout_stream->end_of_stream());
+ EXPECT_EQ("Status: 200\r\nContent-Length: 12\r\n\r\nHello World!",
+ response.stdout);
+ EXPECT_EQ(std::string_view("\0\0\0\0\0\0\0\0", 8), response.ended);
+ if (!cli->closed())
+ cli->wait(logger());
+ EXPECT_TRUE(cli->closed());
+}
+
+TEST_F(TransportFcgiTest, reuse_conn) {
+ write_file("foobar");
+
+ auto pair = create_pair();
+ ASSERT_TRUE(pair.first && pair.second);
+ transport()->add_client(std::move(pair.first));
+ auto cli = create_client(std::move(pair.second));
+
+ make_request(cli.get(), 1, "/hello_world", fcgi::KeepConn);
+
+ Response response;
+
+ while (true) {
+ bool need_more;
+ read_response(cli.get(), 1, &response, &need_more);
+ if (need_more) {
+ cli->wait(logger());
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ ASSERT_TRUE(response.stdout_stream);
+ EXPECT_TRUE(response.stdout_stream->end_of_stream());
+ EXPECT_EQ("Status: 200\r\nContent-Length: 12\r\n\r\nHello World!",
+ response.stdout);
+ EXPECT_EQ(std::string_view("\0\0\0\0\0\0\0\0", 8), response.ended);
+
+ make_request(cli.get(), 2, "/file", fcgi::KeepConn);
+
+ response.reset();
+
+ while (true) {
+ bool need_more;
+ read_response(cli.get(), 2, &response, &need_more);
+ if (need_more) {
+ cli->wait(logger());
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ ASSERT_TRUE(response.stdout_stream);
+ EXPECT_TRUE(response.stdout_stream->end_of_stream());
+ EXPECT_EQ("Status: 200\r\n\r\nfoobar", response.stdout);
+ EXPECT_EQ(std::string_view("\0\0\0\0\0\0\0\0", 8), response.ended);
+}
+
+TEST_F(TransportFcgiTest, multiplexed) {
+ write_file("foobar");
+
+ auto pair = create_pair();
+ ASSERT_TRUE(pair.first && pair.second);
+ transport()->add_client(std::move(pair.first));
+ auto cli = create_client(std::move(pair.second));
+
+ for (size_t step = 0; step < 4; ++step) {
+ make_request(cli.get(), 1, "/hello_world", fcgi::KeepConn, step);
+ make_request(cli.get(), 2, "/file", fcgi::KeepConn, step);
+ }
+
+ Response response1;
+ Response response2;
+ std::map<uint16_t, Response*> responses;
+ responses[1] = &response1;
+ responses[2] = &response2;
+
+ while (true) {
+ bool need_more;
+ read_responses(cli.get(), responses, &need_more);
+ if (need_more) {
+ cli->wait(logger());
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ ASSERT_TRUE(response1.stdout_stream);
+ EXPECT_TRUE(response1.stdout_stream->end_of_stream());
+ EXPECT_EQ("Status: 200\r\nContent-Length: 12\r\n\r\nHello World!",
+ response1.stdout);
+ EXPECT_EQ(std::string_view("\0\0\0\0\0\0\0\0", 8), response1.ended);
+
+ ASSERT_TRUE(response2.stdout_stream);
+ EXPECT_TRUE(response2.stdout_stream->end_of_stream());
+ EXPECT_EQ("Status: 200\r\n\r\nfoobar", response2.stdout);
+ EXPECT_EQ(std::string_view("\0\0\0\0\0\0\0\0", 8), response2.ended);
+}
+
+TEST_F(TransportFcgiTest, get_values) {
+ auto pair = create_pair();
+ ASSERT_TRUE(pair.first && pair.second);
+ transport()->add_client(std::move(pair.first));
+ auto cli = create_client(std::move(pair.second));
+
+ auto pair_builder = fcgi::PairBuilder::create();
+ pair_builder->add("FCGI_MAX_CONNS", "");
+ pair_builder->add("FCGI_MAX_REQS", "");
+ pair_builder->add("foobar", "");
+ auto builder = fcgi::RecordBuilder::create(fcgi::RecordType::GetValues,
+ 0,
+ pair_builder->size());
+ cli->write([&] (Buffer* buf) {
+ ASSERT_TRUE(builder->build(buf));
+ ASSERT_TRUE(pair_builder->build(buf));
+ ASSERT_TRUE(builder->padding(buf));
+ });
+
+ GetValuesResponse response;
+
+ while (true) {
+ bool need_more;
+ read_response(cli.get(), &response, &need_more);
+ if (need_more) {
+ cli->wait(logger());
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ ASSERT_TRUE(response.stream);
+ EXPECT_TRUE(response.stream->end_of_stream());
+ EXPECT_EQ(2, response.values.size());
+ for (auto const& value_pair : response.values) {
+ if (value_pair.first == "FCGI_MAX_REQS") {
+ EXPECT_EQ("20", value_pair.second);
+ } else if (value_pair.first == "FCGI_MAX_CONNS") {
+ EXPECT_EQ("10", value_pair.second);
+ } else {
+ EXPECT_EQ("FCGI_MAX_REQS", value_pair.first);
+ }
+ }
+}
diff --git a/test/test_transport_http.cc b/test/test_transport_http.cc
new file mode 100644
index 0000000..9ce7820
--- /dev/null
+++ b/test/test_transport_http.cc
@@ -0,0 +1,241 @@
+#include "common.hh"
+
+#include "config.hh"
+#include "file_test.hh"
+#include "http_protocol.hh"
+#include "io.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include "socket_test.hh"
+#include "str_buffer.hh"
+#include "strutil.hh"
+#include "task_runner.hh"
+#include "transport_http.hh"
+
+#include <gtest/gtest.h>
+
+namespace {
+
+class TransportHttpTest : public SocketTest, public Transport::Handler {
+public:
+ ~TransportHttpTest() override {
+ if (!path_.empty()) {
+ std::error_code err;
+ std::filesystem::remove(path_, err);
+ }
+ }
+
+ void SetUp() override {
+ fd_ = FileTest::create_temp_file(std::string(), &path_);
+
+ auto config = Config::create_empty();
+ auto factory = create_transport_factory_http();
+ handler_ = Transport::create_default_handler(logger_, this);
+ transport_ = factory->create(logger_, looper(), runner_, logger_.get(),
+ config.get(), handler_.get());
+ }
+
+ void TearDown() override {
+ transport_.reset();
+ handler_.reset();
+ runner_.reset();
+ }
+
+ void write_file(std::string_view content) {
+ ASSERT_TRUE(fd_);
+ auto buffer = make_strbuffer(content);
+ while (!buffer->empty()) {
+ ASSERT_TRUE(io::drain(buffer.get(), fd_.get()));
+ }
+ ASSERT_TRUE(io::close(fd_.release()));
+ }
+
+ std::unique_ptr<Transport::Response> request(
+ Transport* transport, Transport::Request const* request) override {
+ if (request->method() == "GET") {
+ if (request->path() == "/hello_world")
+ return transport->create_ok_data("Hello World!");
+ if (request->path() == "/file")
+ return transport->create_ok_file(path_);
+ }
+ return transport->create_not_found();;
+ }
+
+ Transport* transport() { return transport_.get(); }
+
+ Logger* logger() { return logger_.get(); }
+
+private:
+ std::shared_ptr<Logger> logger_ = Logger::create_null();
+ std::shared_ptr<TaskRunner> runner_ = TaskRunner::create(looper());
+ std::unique_ptr<Transport::Handler> handler_;
+ std::unique_ptr<Transport> transport_;
+ std::filesystem::path path_;
+ unique_fd fd_;
+};
+
+void make_request(SocketTest::Client* cli, std::string const& path,
+ bool keep_conn) {
+ auto builder = HttpRequestBuilder::create(
+ "GET", path, "HTTP", Version(1, 1));
+ if (!keep_conn) {
+ builder->add_header("Connection", "close");
+ }
+ cli->write([&] (Buffer* buf) {
+ ASSERT_TRUE(builder->build(buf));
+ });
+}
+
+struct Response {
+ std::unique_ptr<HttpResponse> response;
+ std::string content;
+ bool ended{false};
+
+ void reset() {
+ response.reset();
+ content.clear();
+ ended = false;
+ }
+};
+
+void read_response_content(SocketTest::Client* cli, Response* response) {
+ assert(!response->ended);
+ assert(response->response);
+ auto len_str = response->response->first_header("content-length");
+ auto size = str::parse_uint64(len_str);
+ if (size) {
+ size_t need = *size - response->content.size();
+ if (need == 0) {
+ response->ended = true;
+ return;
+ }
+ auto received = cli->received();
+ if (received.size() < need) {
+ response->content.append(received);
+ cli->forget(received.size());
+ return;
+ }
+ response->ended = true;
+ response->content.append(received.substr(0, need));
+ cli->forget(need);
+ return;
+ }
+
+ size_t old = response->content.size();
+ response->content.append(cli->received());
+ cli->forget(response->content.size() - old);
+ if (cli->closed())
+ response->ended = true;
+}
+
+void read_response(SocketTest::Client* cli, Response* response,
+ bool* need_more) {
+ if (!response->response) {
+ cli->read([&] (RoBuffer* buf) {
+ response->response = HttpResponse::parse(buf);
+ });
+ if (!response->response) {
+ ASSERT_FALSE(cli->closed());
+ *need_more = true;
+ return;
+ }
+ ASSERT_TRUE(response->response->good());
+ }
+ read_response_content(cli, response);
+ if (!response->ended) {
+ ASSERT_FALSE(cli->closed());
+ *need_more = true;
+ return;
+ }
+ *need_more = false;
+}
+
+} // namespace
+
+TEST_F(TransportHttpTest, sanity) {
+ auto pair = create_pair();
+ ASSERT_TRUE(pair.first && pair.second);
+ transport()->add_client(std::move(pair.first));
+ auto cli = create_client(std::move(pair.second));
+
+ make_request(cli.get(), "/hello_world", false);
+
+ Response response;
+
+ while (true) {
+ bool need_more;
+ read_response(cli.get(), &response, &need_more);
+ if (need_more) {
+ cli->wait(logger());
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ ASSERT_TRUE(response.response);
+ EXPECT_EQ(200, response.response->status_code());
+ EXPECT_EQ("OK", response.response->status_message());
+ EXPECT_EQ("HTTP", response.response->proto());
+ EXPECT_EQ(1, response.response->proto_version().major);
+ EXPECT_EQ(1, response.response->proto_version().minor);
+ EXPECT_EQ("Hello World!", response.content);
+ if (!cli->closed())
+ cli->wait(logger());
+ EXPECT_TRUE(cli->closed());
+}
+
+TEST_F(TransportHttpTest, reuse_conn) {
+ write_file("foobar");
+
+ auto pair = create_pair();
+ ASSERT_TRUE(pair.first && pair.second);
+ transport()->add_client(std::move(pair.first));
+ auto cli = create_client(std::move(pair.second));
+
+ make_request(cli.get(), "/hello_world", true);
+
+ Response response;
+
+ while (true) {
+ bool need_more;
+ read_response(cli.get(), &response, &need_more);
+ if (need_more) {
+ cli->wait(logger());
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ ASSERT_TRUE(response.response);
+ EXPECT_EQ(200, response.response->status_code());
+ EXPECT_EQ("OK", response.response->status_message());
+ EXPECT_EQ("HTTP", response.response->proto());
+ EXPECT_EQ(1, response.response->proto_version().major);
+ EXPECT_EQ(1, response.response->proto_version().minor);
+ EXPECT_EQ("Hello World!", response.content);
+
+ make_request(cli.get(), "/file", true);
+
+ response.reset();
+
+ while (true) {
+ bool need_more;
+ read_response(cli.get(), &response, &need_more);
+ if (need_more) {
+ cli->wait(logger());
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ ASSERT_TRUE(response.response);
+ EXPECT_EQ(200, response.response->status_code());
+ EXPECT_EQ("OK", response.response->status_message());
+ EXPECT_EQ("HTTP", response.response->proto());
+ EXPECT_EQ(1, response.response->proto_version().major);
+ EXPECT_EQ(1, response.response->proto_version().minor);
+ EXPECT_EQ("foobar", response.content);
+}
diff --git a/test/test_tz_info.cc b/test/test_tz_info.cc
new file mode 100644
index 0000000..962f903
--- /dev/null
+++ b/test/test_tz_info.cc
@@ -0,0 +1,114 @@
+#include "common.hh"
+
+#include "file_test.hh"
+#include "logger.hh"
+#include "tz_info.hh"
+
+#include <gtest/gtest.h>
+
+namespace {
+
+class TzInfoTest : public FileTest {
+public:
+ void Load(char const* data, size_t size) {
+ write(std::string_view(data, size));
+ close();
+ tz_info_ = TzInfo::create(logger_, path().parent_path());
+ }
+
+ std::optional<time_t> get_local_time(time_t utc) {
+ if (!tz_info_)
+ return std::nullopt;
+ return tz_info_->get_local_time(path().filename().c_str(), utc);
+ }
+
+private:
+ std::shared_ptr<Logger> logger_{Logger::create_null()};
+ std::unique_ptr<TzInfo> tz_info_;
+};
+
+constexpr const time_t kHour = 60 * 60;
+
+} // namespace
+
+TEST_F(TzInfoTest, v2_honolulu) {
+ char data[] =
+ "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
+ "\0\0\0\x6"
+ "\0\0\0\x6"
+ "\0\0\0\0"
+ "\0\0\0\x7"
+ "\0\0\0\x6"
+ "\0\0\0\x14"
+ "\x80\0\0\0"
+ "\xbb\x05\x43\x48"
+ "\xbb\x21\x71\x58"
+ "\xcb\x89\x3d\xc8"
+ "\xd2\x23\xf4\x70"
+ "\xd2\x61\x49\x38"
+ "\xd5\x8d\x73\x48"
+ "\1\2\1\3\4\1\5"
+ "\xff\xff\x6c\x02\0\x00"
+ "\xff\xff\x6c\x58\0\x04"
+ "\xff\xff\x7a\x68\1\x08"
+ "\xff\xff\x7a\x68\1\x0c"
+ "\xff\xff\x7a\x68\1\x10"
+ "\xff\xff\x73\x60\0\x04"
+ "LMT\0"
+ "HST\0"
+ "HDT\0"
+ "HWT\0"
+ "HPT\0"
+ "\0\0\0\0\1\0"
+ "\0\0\0\0\1\0"
+
+ "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
+ "\0\0\0\x6"
+ "\0\0\0\x6"
+ "\0\0\0\0"
+ "\0\0\0\x7"
+ "\0\0\0\x6"
+ "\0\0\0\x14"
+ "\xff\xff\xff\xff"
+ "\x74\xe0\x70\xbe"
+ "\xff\xff\xff\xff"
+ "\xbb\x05\x43\x48"
+ "\xff\xff\xff\xff"
+ "\xbb\x21\x71\x58"
+ "\xff\xff\xff\xff"
+ "\xcb\x89\x3d\xc8"
+ "\xff\xff\xff\xff"
+ "\xd2\x23\xf4\x70"
+ "\xff\xff\xff\xff"
+ "\xd2\x61\x49\x38"
+ "\xff\xff\xff\xff"
+ "\xd5\x8d\x73\x48"
+ "\1\2\1\3\4\1\5"
+ "\xff\xff\x6c\x02\0\x00"
+ "\xff\xff\x6c\x58\0\x04"
+ "\xff\xff\x7a\x68\1\x08"
+ "\xff\xff\x7a\x68\1\x0c"
+ "\xff\xff\x7a\x68\1\x10"
+ "\xff\xff\x73\x60\0\x04"
+ "LMT\0"
+ "HST\0"
+ "HDT\0"
+ "HWT\0"
+ "HPT\0"
+ "\0\0\0\0\1\0"
+ "\0\0\0\0\1\0"
+ "\nHST10\n";
+ Load(data, sizeof(data) - 1);
+
+ auto ret = get_local_time(-1156939200); // 1933-05-04T12:00:00Z
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(-1156939200 - 9.5 * kHour, ret.value());
+ }
+
+ ret = get_local_time(1546300800); // 2019-01-01T00:00:00Z
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(1546300800 - 10 * kHour, ret.value());
+ }
+}
diff --git a/test/test_tz_str.cc b/test/test_tz_str.cc
new file mode 100644
index 0000000..a96c4ea
--- /dev/null
+++ b/test/test_tz_str.cc
@@ -0,0 +1,97 @@
+#include "common.hh"
+
+#include "tz_str.hh"
+
+#include <gtest/gtest.h>
+
+namespace {
+
+constexpr const time_t kMin = 60;
+constexpr const time_t kHour = 60 * kMin;
+constexpr const time_t kDay = 24 * kHour;
+
+} // namespace
+
+TEST(tz_str, get_local_time_no_dst) {
+ auto ret = tz::get_local_time("HST10", 0);
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(0 - 10 * kHour, ret.value());
+ }
+
+ ret = tz::get_local_time("HST10", kDay * 180);
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(kDay * 180 - 10 * kHour, ret.value());
+ }
+}
+
+TEST(tz_str, get_local_time_quote) {
+ auto ret = tz::get_local_time("<HST-FOO>10", 0);
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(0 - 10 * kHour, ret.value());
+ }
+}
+
+TEST(tz_str, get_local_time_dst) {
+ auto ret = tz::get_local_time("IST-1GMT0,M10.5.0,M3.5.0/1", 0);
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(0 * kHour, ret.value());
+ }
+
+ ret = tz::get_local_time("IST-1GMT0,M10.5.0,M3.5.0/1", kDay * 180);
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(kDay * 180 + 1 * kHour, ret.value());
+ }
+
+ ret = tz::get_local_time("GMT0IST,M3.5.0/1,M10.5.0", 0);
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(0 * kHour, ret.value());
+ }
+
+ ret = tz::get_local_time("GMT0IST,M3.5.0/1,M10.5.0", kDay * 180);
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(kDay * 180 + 1 * kHour, ret.value());
+ }
+
+ ret = tz::get_local_time("NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0", 0);
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(13 * kHour, ret.value());
+ }
+
+ ret = tz::get_local_time("NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0",
+ kDay * 180);
+ EXPECT_TRUE(ret.has_value());
+ if (ret.has_value()) {
+ EXPECT_EQ(kDay * 180 + 12 * kHour, ret.value());
+ }
+}
+
+TEST(tz_str, get_local_time_bad) {
+ auto ret = tz::get_local_time("<HST-", 0);
+ EXPECT_FALSE(ret.has_value());
+
+ ret = tz::get_local_time("< >1", 0);
+ EXPECT_FALSE(ret.has_value());
+
+ ret = tz::get_local_time("HS10", 0);
+ EXPECT_FALSE(ret.has_value());
+
+ ret = tz::get_local_time("HST", 0);
+ EXPECT_FALSE(ret.has_value());
+
+ ret = tz::get_local_time("HST-300", 0);
+ EXPECT_FALSE(ret.has_value());
+
+ ret = tz::get_local_time("HST-10:300", 0);
+ EXPECT_FALSE(ret.has_value());
+
+ ret = tz::get_local_time("HST-10:00:300", 0);
+ EXPECT_FALSE(ret.has_value());
+}
diff --git a/test/test_urlutil.cc b/test/test_urlutil.cc
new file mode 100644
index 0000000..583370d
--- /dev/null
+++ b/test/test_urlutil.cc
@@ -0,0 +1,95 @@
+#include "common.hh"
+
+#include "urlutil.hh"
+
+#include <gtest/gtest.h>
+
+TEST(urlutil, escape) {
+ EXPECT_EQ("", url::escape(""));
+ EXPECT_EQ("foo", url::escape("foo"));
+ EXPECT_EQ("%2Ffoo%2F", url::escape("/foo/"));
+ EXPECT_EQ("%252Ffoo%252F", url::escape("%2Ffoo%2F"));
+ EXPECT_EQ("%FF%00", url::escape(std::string_view("\xff\x00", 2)));
+ EXPECT_EQ("/foo%20bar/", url::escape("/foo bar/",
+ url::EscapeFlags::KEEP_SLASH));
+}
+
+TEST(urlutil, unescape) {
+ EXPECT_EQ("", url::unescape(""));
+ EXPECT_EQ("foo", url::unescape("foo"));
+ EXPECT_EQ("/foo/", url::unescape("%2Ffoo%2F"));
+ EXPECT_EQ("%2Ffoo%2F", url::unescape("%252Ffoo%252F"));
+ EXPECT_EQ(std::string_view("\xff\x00", 2), url::unescape("%FF%00"));
+}
+
+TEST(urlutil, unescape_invalid) {
+ EXPECT_EQ("%", url::unescape("%"));
+ EXPECT_EQ("%2", url::unescape("%2"));
+ EXPECT_EQ("%2X", url::unescape("%2X"));
+ EXPECT_EQ("%X2", url::unescape("%X2"));
+ EXPECT_EQ("%%%%%", url::unescape("%%%%%"));
+}
+
+TEST(urlutil, expand_and_unescape_query) {
+ auto ret = url::expand_and_unescape_query("");
+ EXPECT_TRUE(ret.empty());
+
+ ret = url::expand_and_unescape_query("foo=bar");
+ EXPECT_EQ(1, ret.size());
+ EXPECT_EQ("bar", ret["foo"]);
+
+ ret = url::expand_and_unescape_query("foo=bar&fum&a=&foo");
+ EXPECT_EQ(3, ret.size());
+ EXPECT_EQ("bar", ret["foo"]);
+ EXPECT_EQ("", ret["fum"]);
+ EXPECT_EQ("", ret["a"]);
+
+ ret = url::expand_and_unescape_query("foo=b%20a%20r&bar=f+o+o");
+ EXPECT_EQ(2, ret.size());
+ EXPECT_EQ("b a r", ret["foo"]);
+ EXPECT_EQ("f o o", ret["bar"]);
+
+ ret = url::expand_and_unescape_query("foo=%26amp%3B&%3D=%25");
+ EXPECT_EQ(2, ret.size());
+ EXPECT_EQ("&amp;", ret["foo"]);
+ EXPECT_EQ("%", ret["="]);
+
+ ret = url::expand_and_unescape_query("=");
+ EXPECT_EQ(1, ret.size());
+ EXPECT_EQ("", ret[""]);
+
+ ret = url::expand_and_unescape_query("&&=&=&&");
+ EXPECT_EQ(1, ret.size());
+ EXPECT_EQ("", ret[""]);
+
+ ret = url::expand_and_unescape_query("=====");
+ EXPECT_EQ(1, ret.size());
+ EXPECT_EQ("====", ret[""]);
+}
+
+TEST(urlutil, split_and_unescape_path_and_query) {
+ std::string path;
+ std::unordered_map<std::string, std::string> query;
+
+ url::split_and_unescape_path_and_query("", path, query);
+ EXPECT_EQ("", path);
+ EXPECT_TRUE(query.empty());
+
+ url::split_and_unescape_path_and_query("foo", path, query);
+ EXPECT_EQ("foo", path);
+ EXPECT_TRUE(query.empty());
+
+ url::split_and_unescape_path_and_query("?foo=bar", path, query);
+ EXPECT_EQ("", path);
+ EXPECT_EQ(1, query.size());
+ EXPECT_EQ("bar", query["foo"]);
+
+ url::split_and_unescape_path_and_query("foo?foo=bar", path, query);
+ EXPECT_EQ("foo", path);
+ EXPECT_EQ(1, query.size());
+ EXPECT_EQ("bar", query["foo"]);
+
+ url::split_and_unescape_path_and_query("%3F%3F%3D", path, query);
+ EXPECT_EQ("\?\?=", path);
+ EXPECT_TRUE(query.empty());
+}
diff --git a/test/test_video.cc b/test/test_video.cc
new file mode 100644
index 0000000..1857940
--- /dev/null
+++ b/test/test_video.cc
@@ -0,0 +1,261 @@
+#include "common.hh"
+
+#include "file_test.hh"
+#include "mock_timezone.hh"
+#include "video.hh"
+
+#include <gtest/gtest.h>
+
+namespace {
+
+static const uint8_t kSmallMp4[] = {
+ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70,
+ 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x02, 0x00,
+ 0x69, 0x73, 0x6f, 0x6d, 0x69, 0x73, 0x6f, 0x32,
+ 0x61, 0x76, 0x63, 0x31, 0x6d, 0x70, 0x34, 0x31,
+ 0x00, 0x00, 0x00, 0x08, 0x66, 0x72, 0x65, 0x65,
+ 0x00, 0x00, 0x02, 0xcc, 0x6d, 0x64, 0x61, 0x74,
+ 0x00, 0x00, 0x02, 0xad, 0x06, 0x05, 0xff, 0xff,
+ 0xa9, 0xdc, 0x45, 0xe9, 0xbd, 0xe6, 0xd9, 0x48,
+ 0xb7, 0x96, 0x2c, 0xd8, 0x20, 0xd9, 0x23, 0xee,
+ 0xef, 0x78, 0x32, 0x36, 0x34, 0x20, 0x2d, 0x20,
+ 0x63, 0x6f, 0x72, 0x65, 0x20, 0x31, 0x36, 0x33,
+ 0x20, 0x72, 0x33, 0x30, 0x36, 0x30, 0x20, 0x35,
+ 0x64, 0x62, 0x36, 0x61, 0x61, 0x36, 0x20, 0x2d,
+ 0x20, 0x48, 0x2e, 0x32, 0x36, 0x34, 0x2f, 0x4d,
+ 0x50, 0x45, 0x47, 0x2d, 0x34, 0x20, 0x41, 0x56,
+ 0x43, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x20,
+ 0x2d, 0x20, 0x43, 0x6f, 0x70, 0x79, 0x6c, 0x65,
+ 0x66, 0x74, 0x20, 0x32, 0x30, 0x30, 0x33, 0x2d,
+ 0x32, 0x30, 0x32, 0x31, 0x20, 0x2d, 0x20, 0x68,
+ 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77,
+ 0x77, 0x2e, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6c,
+ 0x61, 0x6e, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x78,
+ 0x32, 0x36, 0x34, 0x2e, 0x68, 0x74, 0x6d, 0x6c,
+ 0x20, 0x2d, 0x20, 0x6f, 0x70, 0x74, 0x69, 0x6f,
+ 0x6e, 0x73, 0x3a, 0x20, 0x63, 0x61, 0x62, 0x61,
+ 0x63, 0x3d, 0x31, 0x20, 0x72, 0x65, 0x66, 0x3d,
+ 0x33, 0x20, 0x64, 0x65, 0x62, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x3d, 0x31, 0x3a, 0x30, 0x3a, 0x30, 0x20,
+ 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73, 0x65, 0x3d,
+ 0x30, 0x78, 0x33, 0x3a, 0x30, 0x78, 0x31, 0x31,
+ 0x33, 0x20, 0x6d, 0x65, 0x3d, 0x68, 0x65, 0x78,
+ 0x20, 0x73, 0x75, 0x62, 0x6d, 0x65, 0x3d, 0x37,
+ 0x20, 0x70, 0x73, 0x79, 0x3d, 0x31, 0x20, 0x70,
+ 0x73, 0x79, 0x5f, 0x72, 0x64, 0x3d, 0x31, 0x2e,
+ 0x30, 0x30, 0x3a, 0x30, 0x2e, 0x30, 0x30, 0x20,
+ 0x6d, 0x69, 0x78, 0x65, 0x64, 0x5f, 0x72, 0x65,
+ 0x66, 0x3d, 0x31, 0x20, 0x6d, 0x65, 0x5f, 0x72,
+ 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x31, 0x36, 0x20,
+ 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x61, 0x5f, 0x6d,
+ 0x65, 0x3d, 0x31, 0x20, 0x74, 0x72, 0x65, 0x6c,
+ 0x6c, 0x69, 0x73, 0x3d, 0x31, 0x20, 0x38, 0x78,
+ 0x38, 0x64, 0x63, 0x74, 0x3d, 0x31, 0x20, 0x63,
+ 0x71, 0x6d, 0x3d, 0x30, 0x20, 0x64, 0x65, 0x61,
+ 0x64, 0x7a, 0x6f, 0x6e, 0x65, 0x3d, 0x32, 0x31,
+ 0x2c, 0x31, 0x31, 0x20, 0x66, 0x61, 0x73, 0x74,
+ 0x5f, 0x70, 0x73, 0x6b, 0x69, 0x70, 0x3d, 0x31,
+ 0x20, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x61, 0x5f,
+ 0x71, 0x70, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65,
+ 0x74, 0x3d, 0x34, 0x20, 0x74, 0x68, 0x72, 0x65,
+ 0x61, 0x64, 0x73, 0x3d, 0x31, 0x20, 0x6c, 0x6f,
+ 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x5f,
+ 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d,
+ 0x31, 0x20, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x64,
+ 0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73,
+ 0x3d, 0x30, 0x20, 0x6e, 0x72, 0x3d, 0x30, 0x20,
+ 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x74, 0x65,
+ 0x3d, 0x31, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72,
+ 0x6c, 0x61, 0x63, 0x65, 0x64, 0x3d, 0x30, 0x20,
+ 0x62, 0x6c, 0x75, 0x72, 0x61, 0x79, 0x5f, 0x63,
+ 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x3d, 0x30, 0x20,
+ 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69,
+ 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x6e, 0x74, 0x72,
+ 0x61, 0x3d, 0x30, 0x20, 0x62, 0x66, 0x72, 0x61,
+ 0x6d, 0x65, 0x73, 0x3d, 0x33, 0x20, 0x62, 0x5f,
+ 0x70, 0x79, 0x72, 0x61, 0x6d, 0x69, 0x64, 0x3d,
+ 0x32, 0x20, 0x62, 0x5f, 0x61, 0x64, 0x61, 0x70,
+ 0x74, 0x3d, 0x31, 0x20, 0x62, 0x5f, 0x62, 0x69,
+ 0x61, 0x73, 0x3d, 0x30, 0x20, 0x64, 0x69, 0x72,
+ 0x65, 0x63, 0x74, 0x3d, 0x31, 0x20, 0x77, 0x65,
+ 0x69, 0x67, 0x68, 0x74, 0x62, 0x3d, 0x31, 0x20,
+ 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x67, 0x6f, 0x70,
+ 0x3d, 0x30, 0x20, 0x77, 0x65, 0x69, 0x67, 0x68,
+ 0x74, 0x70, 0x3d, 0x32, 0x20, 0x6b, 0x65, 0x79,
+ 0x69, 0x6e, 0x74, 0x3d, 0x32, 0x35, 0x30, 0x20,
+ 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x5f, 0x6d,
+ 0x69, 0x6e, 0x3d, 0x32, 0x35, 0x20, 0x73, 0x63,
+ 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x3d, 0x34,
+ 0x30, 0x20, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x5f,
+ 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x3d,
+ 0x30, 0x20, 0x72, 0x63, 0x5f, 0x6c, 0x6f, 0x6f,
+ 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x34,
+ 0x30, 0x20, 0x72, 0x63, 0x3d, 0x63, 0x72, 0x66,
+ 0x20, 0x6d, 0x62, 0x74, 0x72, 0x65, 0x65, 0x3d,
+ 0x31, 0x20, 0x63, 0x72, 0x66, 0x3d, 0x32, 0x33,
+ 0x2e, 0x30, 0x20, 0x71, 0x63, 0x6f, 0x6d, 0x70,
+ 0x3d, 0x30, 0x2e, 0x36, 0x30, 0x20, 0x71, 0x70,
+ 0x6d, 0x69, 0x6e, 0x3d, 0x30, 0x20, 0x71, 0x70,
+ 0x6d, 0x61, 0x78, 0x3d, 0x36, 0x39, 0x20, 0x71,
+ 0x70, 0x73, 0x74, 0x65, 0x70, 0x3d, 0x34, 0x20,
+ 0x69, 0x70, 0x5f, 0x72, 0x61, 0x74, 0x69, 0x6f,
+ 0x3d, 0x31, 0x2e, 0x34, 0x30, 0x20, 0x61, 0x71,
+ 0x3d, 0x31, 0x3a, 0x31, 0x2e, 0x30, 0x30, 0x00,
+ 0x80, 0x00, 0x00, 0x00, 0x0f, 0x65, 0x88, 0x84,
+ 0x00, 0x2b, 0xff, 0xfe, 0xf5, 0xdb, 0xf3, 0x2c,
+ 0x9c, 0x35, 0x6f, 0xf9, 0x00, 0x00, 0x03, 0x1b,
+ 0x6d, 0x6f, 0x6f, 0x76, 0x00, 0x00, 0x00, 0x6c,
+ 0x6d, 0x76, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x28,
+ 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+ 0x00, 0x00, 0x02, 0x45, 0x74, 0x72, 0x61, 0x6b,
+ 0x00, 0x00, 0x00, 0x5c, 0x74, 0x6b, 0x68, 0x64,
+ 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x40, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24,
+ 0x65, 0x64, 0x74, 0x73, 0x00, 0x00, 0x00, 0x1c,
+ 0x65, 0x6c, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x28,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x01, 0xbd, 0x6d, 0x64, 0x69, 0x61,
+ 0x00, 0x00, 0x00, 0x20, 0x6d, 0x64, 0x68, 0x64,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00,
+ 0x00, 0x00, 0x02, 0x00, 0x55, 0xc4, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x2d, 0x68, 0x64, 0x6c, 0x72,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x76, 0x69, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e,
+ 0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01,
+ 0x68, 0x6d, 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00,
+ 0x14, 0x76, 0x6d, 0x68, 0x64, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e,
+ 0x66, 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65,
+ 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c,
+ 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01,
+ 0x28, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00,
+ 0xc4, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0xb4, 0x61, 0x76, 0x63, 0x31, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
+ 0x01, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3a, 0x61,
+ 0x76, 0x63, 0x43, 0x01, 0xf4, 0x00, 0x0a, 0xff,
+ 0xe1, 0x00, 0x1d, 0x67, 0xf4, 0x00, 0x0a, 0x91,
+ 0x9b, 0x2b, 0xf0, 0x84, 0x21, 0x80, 0xb7, 0x02,
+ 0x02, 0x05, 0x40, 0x00, 0x00, 0x03, 0x00, 0x40,
+ 0x00, 0x00, 0x0c, 0x83, 0xc4, 0x89, 0x65, 0x80,
+ 0x01, 0x00, 0x06, 0x68, 0xeb, 0xe3, 0xc4, 0x48,
+ 0x44, 0xff, 0xf8, 0xf8, 0x00, 0x00, 0x00, 0x00,
+ 0x10, 0x70, 0x61, 0x73, 0x70, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x14, 0x62, 0x74, 0x72, 0x74, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x02, 0x29, 0x20, 0x00, 0x02, 0x29,
+ 0x20, 0x00, 0x00, 0x00, 0x18, 0x73, 0x74, 0x74,
+ 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02,
+ 0x00, 0x00, 0x00, 0x00, 0x1c, 0x73, 0x74, 0x73,
+ 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x14, 0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x02, 0xc4, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x14, 0x73, 0x74, 0x63,
+ 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
+ 0x62, 0x75, 0x64, 0x74, 0x61, 0x00, 0x00, 0x00,
+ 0x5a, 0x6d, 0x65, 0x74, 0x61, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6c,
+ 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x6d, 0x64, 0x69, 0x72, 0x61, 0x70, 0x70,
+ 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x69, 0x6c,
+ 0x73, 0x74, 0x00, 0x00, 0x00, 0x25, 0xa9, 0x74,
+ 0x6f, 0x6f, 0x00, 0x00, 0x00, 0x1d, 0x64, 0x61,
+ 0x74, 0x61, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x4c, 0x61, 0x76, 0x66, 0x35, 0x38,
+ 0x2e, 0x37, 0x36, 0x2e, 0x31, 0x30, 0x30,
+};
+
+class VideoTest : public FileTest {
+public:
+ void write_small_mp4() {
+ write(std::string_view(reinterpret_cast<char const*>(kSmallMp4),
+ sizeof(kSmallMp4)));
+ close();
+ }
+
+ void write_bad_mp4() {
+ write(std::string_view(reinterpret_cast<char const*>(kSmallMp4),
+ sizeof(kSmallMp4) / 2));
+ close();
+ }
+
+ std::string const& extension() override {
+ static std::string mp4 = ".mp4";
+ return mp4;
+ }
+
+ void SetUp() override {
+ FileTest::SetUp();
+ }
+
+ Timezone* timezone() {
+ return &timezone_;
+ }
+
+private:
+ MockTimezone timezone_;
+};
+
+} // namespace
+
+TEST_F(VideoTest, small_mp4) {
+ write_small_mp4();
+ auto vid = Video::load(path(), timezone());
+ ASSERT_TRUE(vid);
+ EXPECT_EQ(1, vid->width());
+ EXPECT_EQ(1, vid->height());
+ EXPECT_EQ(0.04, vid->length());
+ EXPECT_TRUE(vid->location().empty());
+ EXPECT_TRUE(vid->date().empty());
+}
+
+TEST_F(VideoTest, bad_video) {
+ write_bad_mp4();
+ auto vid = Video::load(path(), timezone());
+ EXPECT_FALSE(vid);
+}
+
+TEST_F(VideoTest, non_existant) {
+ auto vid = Video::load(path() / "non_existant", timezone());
+ EXPECT_FALSE(vid);
+}