diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2021-11-17 22:34:57 +0100 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2021-11-17 22:34:57 +0100 |
| commit | 6232d13f5321b87ddf12a1aa36b4545da45f173d (patch) | |
| tree | 23f3316470a14136debd9d02f9e920ca2b06f4cc /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')
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("<foo>", html::escape("<foo>")); + EXPECT_EQ("foo & bar", html::escape("foo & bar")); + EXPECT_EQ("\"&lt;\" vs '&gt;'", + html::escape("\"<\" vs '>'")); + EXPECT_EQ("<<<", html::escape("<<<")); +} + +TEST(htmlutil, escape_attribute) { + EXPECT_EQ("", html::escape("", html::EscapeTarget::ATTRIBUTE)); + EXPECT_EQ("foo", html::escape("foo", html::EscapeTarget::ATTRIBUTE)); + EXPECT_EQ("<foo>", html::escape("<foo>", html::EscapeTarget::ATTRIBUTE)); + EXPECT_EQ("foo & bar", html::escape("foo & bar", + html::EscapeTarget::ATTRIBUTE)); + EXPECT_EQ(""&lt;" vs '&gt;'", + html::escape("\"<\" vs '>'", html::EscapeTarget::ATTRIBUTE)); + EXPECT_EQ("<<<", 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 <b>World</b>!</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('Hello World');\">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("&", 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); +} |
