feat: add Linux OAuth adapters
Add a bounded libcurl transport with verified TLS, no redirects or retries, sanitized failures, binary-safe requests, and resolved-address enforcement for cleartext loopback traffic. Add shell-free xdg-open launching and a one-shot IPv4 loopback callback receiver with strict HTTP/query parsing, deadlines, fixed callback path, and RAII cleanup. Token parsing, Secret Service, and Graph remain follow-up phases.
This commit is contained in:
517
tests/desktop_oauth_tests.cpp
Normal file
517
tests/desktop_oauth_tests.cpp
Normal file
@@ -0,0 +1,517 @@
|
||||
#include "nocal/sync/desktop_oauth.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using nocal::sync::AuthorizationCallback;
|
||||
using nocal::sync::LoopbackCallbackReceiver;
|
||||
using nocal::sync::XdgBrowserLauncher;
|
||||
|
||||
void require(bool condition, std::string_view message) {
|
||||
if (!condition) {
|
||||
throw std::runtime_error(std::string{message});
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Function>
|
||||
void require_throws(Function&& function, std::string_view message) {
|
||||
try {
|
||||
function();
|
||||
} catch (const std::exception&) {
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error(std::string{message});
|
||||
}
|
||||
|
||||
class TempDirectory {
|
||||
public:
|
||||
TempDirectory() {
|
||||
std::string pattern =
|
||||
(std::filesystem::temp_directory_path() / "nocal-desktop-oauth-tests.XXXXXX")
|
||||
.string();
|
||||
std::vector<char> writable(pattern.begin(), pattern.end());
|
||||
writable.push_back('\0');
|
||||
const char* created = ::mkdtemp(writable.data());
|
||||
if (created == nullptr) {
|
||||
throw std::runtime_error("mkdtemp failed");
|
||||
}
|
||||
path_ = created;
|
||||
}
|
||||
|
||||
TempDirectory(const TempDirectory&) = delete;
|
||||
TempDirectory& operator=(const TempDirectory&) = delete;
|
||||
|
||||
~TempDirectory() {
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove_all(path_, ignored);
|
||||
}
|
||||
|
||||
[[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; }
|
||||
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
};
|
||||
|
||||
class Descriptor {
|
||||
public:
|
||||
explicit Descriptor(int value = -1) : value_(value) {}
|
||||
~Descriptor() {
|
||||
if (value_ >= 0) {
|
||||
static_cast<void>(::close(value_));
|
||||
}
|
||||
}
|
||||
|
||||
Descriptor(const Descriptor&) = delete;
|
||||
Descriptor& operator=(const Descriptor&) = delete;
|
||||
Descriptor(Descriptor&& other) noexcept : value_(std::exchange(other.value_, -1)) {}
|
||||
|
||||
Descriptor& operator=(Descriptor&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (value_ >= 0) {
|
||||
static_cast<void>(::close(value_));
|
||||
}
|
||||
value_ = std::exchange(other.value_, -1);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] int get() const noexcept { return value_; }
|
||||
|
||||
private:
|
||||
int value_;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::string read_file(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
require(static_cast<bool>(input), "could not read browser helper output");
|
||||
return {std::istreambuf_iterator<char>{input}, {}};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string self_executable() {
|
||||
return std::filesystem::read_symlink("/proc/self/exe").string();
|
||||
}
|
||||
|
||||
void set_helper_output(const std::filesystem::path& path) {
|
||||
require(::setenv("NOCAL_DESKTOP_OAUTH_HELPER_OUTPUT", path.c_str(), 1) == 0,
|
||||
"could not configure browser helper output");
|
||||
}
|
||||
|
||||
int run_browser_helper(int argc, char** argv) {
|
||||
const char* output_path = std::getenv("NOCAL_DESKTOP_OAUTH_HELPER_OUTPUT");
|
||||
if (output_path == nullptr) {
|
||||
return 91;
|
||||
}
|
||||
std::ofstream output(output_path, std::ios::binary | std::ios::trunc);
|
||||
output << argc << '\n';
|
||||
for (int index = 0; index < argc; ++index) {
|
||||
output << argv[index] << '\n';
|
||||
}
|
||||
if (!output) {
|
||||
return 92;
|
||||
}
|
||||
return argc == 2 && std::string_view{argv[1]}.starts_with("browser-helper:exit=7") ? 7 : 0;
|
||||
}
|
||||
|
||||
void test_browser_literal_argv_and_failures(const std::filesystem::path& root) {
|
||||
const auto helper_output = root / "browser-argv.txt";
|
||||
const auto injection_target = root / "must-not-exist";
|
||||
set_helper_output(helper_output);
|
||||
XdgBrowserLauncher launcher{self_executable()};
|
||||
const std::string url = "browser-helper:exit=0; literal argument; touch "
|
||||
+ injection_target.string() + "; $(touch " + injection_target.string() + ")";
|
||||
launcher.open(url);
|
||||
const std::string report = read_file(helper_output);
|
||||
require(report.starts_with("2\n"), "browser helper received more than one URL argument");
|
||||
require(report.ends_with(url + "\n"), "browser helper did not receive the URL literally");
|
||||
require(!std::filesystem::exists(injection_target), "browser URL was interpreted by a shell");
|
||||
|
||||
const std::string sensitive = "SENSITIVE_BROWSER_URL";
|
||||
try {
|
||||
launcher.open("browser-helper:exit=7;" + sensitive);
|
||||
throw std::runtime_error("nonzero browser exit was accepted");
|
||||
} catch (const std::exception& error) {
|
||||
require(std::string_view{error.what()}.find(sensitive) == std::string_view::npos,
|
||||
"browser URL leaked through an exception");
|
||||
}
|
||||
|
||||
require_throws([] { XdgBrowserLauncher invalid{""}; },
|
||||
"empty browser executable was accepted");
|
||||
require_throws(
|
||||
[] { XdgBrowserLauncher invalid{std::string{"browser\0name", 12}}; },
|
||||
"browser executable containing NUL was accepted");
|
||||
require_throws(
|
||||
[] {
|
||||
XdgBrowserLauncher missing{"nocal-browser-executable-that-does-not-exist"};
|
||||
missing.open("https://example.test");
|
||||
},
|
||||
"missing browser executable was accepted");
|
||||
|
||||
for (const std::string& invalid_url : {
|
||||
std::string{"https://example.test/\0secret", 28},
|
||||
std::string{"https://example.test/\nsecret"},
|
||||
std::string{"https://example.test/\rsecret"},
|
||||
std::string{"https://example.test/\x7fsecret"},
|
||||
}) {
|
||||
require_throws([&] { launcher.open(invalid_url); },
|
||||
"browser URL containing a control character was accepted");
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] unsigned int port_from_uri(std::string_view uri) {
|
||||
constexpr std::string_view prefix = "http://localhost:";
|
||||
constexpr std::string_view suffix = "/nocal/oauth/callback";
|
||||
require(uri.starts_with(prefix) && uri.ends_with(suffix),
|
||||
"loopback redirect URI has the wrong shape");
|
||||
const std::string_view encoded = uri.substr(prefix.size(), uri.size() - prefix.size() - suffix.size());
|
||||
require(!encoded.empty(), "loopback redirect URI has no port");
|
||||
unsigned int port = 0;
|
||||
for (const char character : encoded) {
|
||||
require(character >= '0' && character <= '9', "loopback redirect port is not numeric");
|
||||
port = port * 10U + static_cast<unsigned int>(character - '0');
|
||||
}
|
||||
require(port > 0 && port <= 65'535, "loopback redirect port is out of range");
|
||||
return port;
|
||||
}
|
||||
|
||||
[[nodiscard]] Descriptor connect_ipv4(unsigned int port) {
|
||||
Descriptor client{::socket(AF_INET, SOCK_STREAM, 0)};
|
||||
require(client.get() >= 0, "could not create loopback test socket");
|
||||
sockaddr_in address{};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
address.sin_port = htons(static_cast<std::uint16_t>(port));
|
||||
require(::connect(client.get(), reinterpret_cast<const sockaddr*>(&address), sizeof(address))
|
||||
== 0,
|
||||
"could not connect to loopback receiver");
|
||||
return client;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool can_connect_ipv4(unsigned int port) {
|
||||
Descriptor client{::socket(AF_INET, SOCK_STREAM, 0)};
|
||||
if (client.get() < 0) {
|
||||
return false;
|
||||
}
|
||||
sockaddr_in address{};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
address.sin_port = htons(static_cast<std::uint16_t>(port));
|
||||
return ::connect(client.get(), reinterpret_cast<const sockaddr*>(&address), sizeof(address))
|
||||
== 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool can_connect_ipv6(unsigned int port) {
|
||||
Descriptor client{::socket(AF_INET6, SOCK_STREAM, 0)};
|
||||
if (client.get() < 0) {
|
||||
return false;
|
||||
}
|
||||
sockaddr_in6 address{};
|
||||
address.sin6_family = AF_INET6;
|
||||
address.sin6_addr = in6addr_loopback;
|
||||
address.sin6_port = htons(static_cast<std::uint16_t>(port));
|
||||
return ::connect(client.get(), reinterpret_cast<const sockaddr*>(&address), sizeof(address))
|
||||
== 0;
|
||||
}
|
||||
|
||||
void send_bytes(int descriptor, std::string_view bytes) {
|
||||
std::size_t offset = 0;
|
||||
while (offset < bytes.size()) {
|
||||
const ssize_t sent =
|
||||
::send(descriptor, bytes.data() + offset, bytes.size() - offset, MSG_NOSIGNAL);
|
||||
if (sent < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
require(sent > 0, "failed to send loopback test request");
|
||||
offset += static_cast<std::size_t>(sent);
|
||||
}
|
||||
}
|
||||
|
||||
void send_all(int descriptor, std::string_view bytes) {
|
||||
send_bytes(descriptor, bytes);
|
||||
require(::shutdown(descriptor, SHUT_WR) == 0, "failed to finish loopback test request");
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string read_response(int descriptor) {
|
||||
std::string result;
|
||||
std::array<char, 1024> buffer{};
|
||||
while (true) {
|
||||
const ssize_t received = ::recv(descriptor, buffer.data(), buffer.size(), 0);
|
||||
if (received > 0) {
|
||||
result.append(buffer.data(), static_cast<std::size_t>(received));
|
||||
} else if (received == 0) {
|
||||
return result;
|
||||
} else if (errno == ECONNRESET && !result.empty()) {
|
||||
return result;
|
||||
} else if (errno != EINTR) {
|
||||
throw std::runtime_error("failed to read loopback HTTP response");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string request_for(unsigned int port, std::string_view target,
|
||||
std::string_view method = "GET", std::string_view extra_headers = {}) {
|
||||
return std::string{method} + " " + std::string{target} + " HTTP/1.1\r\nHost: localhost:"
|
||||
+ std::to_string(port) + "\r\n" + std::string{extra_headers} + "\r\n";
|
||||
}
|
||||
|
||||
struct ExchangeResult {
|
||||
AuthorizationCallback callback;
|
||||
std::string response;
|
||||
};
|
||||
|
||||
[[nodiscard]] ExchangeResult valid_exchange(
|
||||
LoopbackCallbackReceiver& receiver, std::string_view query) {
|
||||
const unsigned int port = port_from_uri(receiver.redirect_uri());
|
||||
Descriptor client = connect_ipv4(port);
|
||||
send_all(client.get(), request_for(port, std::string{"/nocal/oauth/callback?"} + std::string{query}));
|
||||
AuthorizationCallback callback = receiver.receive();
|
||||
return {std::move(callback), read_response(client.get())};
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string invalid_exchange(
|
||||
LoopbackCallbackReceiver& receiver, const std::string& request) {
|
||||
const unsigned int port = port_from_uri(receiver.redirect_uri());
|
||||
Descriptor client = connect_ipv4(port);
|
||||
send_all(client.get(), request);
|
||||
require_throws([&] { static_cast<void>(receiver.receive()); },
|
||||
"invalid loopback request was accepted");
|
||||
return read_response(client.get());
|
||||
}
|
||||
|
||||
void require_success_response(std::string_view response) {
|
||||
require(response.starts_with("HTTP/1.1 200 OK\r\n"),
|
||||
"valid callback did not receive HTTP 200");
|
||||
require(response.find("Cache-Control: no-store") != std::string_view::npos,
|
||||
"valid callback response is cacheable");
|
||||
}
|
||||
|
||||
void require_failure_response(std::string_view response) {
|
||||
require(response.starts_with("HTTP/1.1 400 Bad Request\r\n"),
|
||||
"invalid callback did not receive HTTP 400");
|
||||
require(response.find("Cache-Control: no-store") != std::string_view::npos,
|
||||
"invalid callback response is cacheable");
|
||||
require(response.find("SENSITIVE_CALLBACK") == std::string_view::npos,
|
||||
"invalid callback response reflected request data");
|
||||
}
|
||||
|
||||
void test_redirect_and_parsed_callbacks() {
|
||||
LoopbackCallbackReceiver success{1s};
|
||||
const unsigned int port = port_from_uri(success.redirect_uri());
|
||||
require(!can_connect_ipv6(port), "loopback receiver unexpectedly listens on IPv6");
|
||||
const auto accepted =
|
||||
valid_exchange(success, "code=abc%2Bdef&state=state+with+spaces&session_state=ignored");
|
||||
require(accepted.callback.code == "abc+def" && accepted.callback.state == "state with spaces",
|
||||
"successful callback query was not decoded correctly");
|
||||
require(accepted.callback.error.empty() && accepted.callback.error_description.empty(),
|
||||
"unknown callback field contaminated known fields");
|
||||
require_success_response(accepted.response);
|
||||
|
||||
LoopbackCallbackReceiver denied{1s};
|
||||
const auto rejected = valid_exchange(denied,
|
||||
"error=access_denied&error_description=User+cancelled&state=state-2&unknown=value");
|
||||
require(rejected.callback.code.empty() && rejected.callback.error == "access_denied"
|
||||
&& rejected.callback.error_description == "User cancelled"
|
||||
&& rejected.callback.state == "state-2",
|
||||
"authorization error callback was not decoded correctly");
|
||||
require_success_response(rejected.response);
|
||||
|
||||
LoopbackCallbackReceiver uppercase_host{1s};
|
||||
const unsigned int uppercase_port = port_from_uri(uppercase_host.redirect_uri());
|
||||
Descriptor uppercase_client = connect_ipv4(uppercase_port);
|
||||
send_all(uppercase_client.get(),
|
||||
"GET /nocal/oauth/callback?code=upper&state=case HTTP/1.1\r\nhOsT: LOCALHOST:"
|
||||
+ std::to_string(uppercase_port) + "\r\n\r\n");
|
||||
const auto uppercase_callback = uppercase_host.receive();
|
||||
require(uppercase_callback.code == "upper" && uppercase_callback.state == "case",
|
||||
"ASCII-case-insensitive localhost Host was rejected");
|
||||
require_success_response(read_response(uppercase_client.get()));
|
||||
}
|
||||
|
||||
void test_invalid_http_matrix() {
|
||||
auto run = [](const std::function<std::string(unsigned int)>& build) {
|
||||
LoopbackCallbackReceiver receiver{1s};
|
||||
const unsigned int port = port_from_uri(receiver.redirect_uri());
|
||||
const std::string response = invalid_exchange(receiver, build(port));
|
||||
require_failure_response(response);
|
||||
require(!can_connect_ipv4(port), "invalid callback left listener open");
|
||||
};
|
||||
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=x&state=y", "POST");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/wrong?code=x&state=y");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return "GET /nocal/oauth/callback?code=x&state=y HTTP/1.0\r\nHost: localhost:"
|
||||
+ std::to_string(port) + "\r\n\r\n";
|
||||
});
|
||||
run([](unsigned int) {
|
||||
return std::string{"GET /nocal/oauth/callback?code=x&state=y HTTP/1.1\r\n\r\n"};
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return "GET /nocal/oauth/callback?code=x&state=y HTTP/1.1\r\nHost: 127.0.0.1:"
|
||||
+ std::to_string(port) + "\r\n\r\n";
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return "GET /nocal/oauth/callback?code=x&state=y HTTP/1.1\r\nHost: localhost.:"
|
||||
+ std::to_string(port) + "\r\n\r\n";
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return "GET /nocal/oauth/callback?code=x&state=y HTTP/1.1\r\nHost: localhost.evil:"
|
||||
+ std::to_string(port) + "\r\n\r\n";
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=x&state=y",
|
||||
"GET", "Host: localhost:" + std::to_string(port) + "\r\n");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=x&state=y", "GET", "Broken\r\n");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=x&state=y", "GET",
|
||||
std::string{"X-"} + "\xC3\xA9" + ": value\r\n");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=x&state=y", "GET",
|
||||
"Transfer-Encoding: chunked\r\n");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=x&state=y", "GET",
|
||||
"Content-Length: 1\r\n")
|
||||
+ "x";
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(
|
||||
port, "/nocal/oauth/callback?code=SENSITIVE_CALLBACK&state=y&state=z");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=%GG&state=y");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=%00&state=y");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=%1F&state=y");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code&state=y");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
std::string target = "/nocal/oauth/callback?code=x&state=y";
|
||||
target.push_back('\0');
|
||||
return request_for(port, target);
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return request_for(port, "/nocal/oauth/callback?code=x&state=y", "GET",
|
||||
"X-Large: " + std::string(17U * 1024U, 'a') + "\r\n");
|
||||
});
|
||||
run([](unsigned int port) {
|
||||
return "GET /nocal/oauth/callback?code=x&state=y HTTP/1.1\nHost: localhost:"
|
||||
+ std::to_string(port) + "\n\n";
|
||||
});
|
||||
}
|
||||
|
||||
void test_timeout_single_use_and_cleanup() {
|
||||
require_throws([] { LoopbackCallbackReceiver invalid{0ms}; },
|
||||
"zero loopback timeout was accepted");
|
||||
require_throws([] { LoopbackCallbackReceiver invalid{-1ms}; },
|
||||
"negative loopback timeout was accepted");
|
||||
|
||||
LoopbackCallbackReceiver timed{20ms};
|
||||
const unsigned int timed_port = port_from_uri(timed.redirect_uri());
|
||||
require_throws([&] { static_cast<void>(timed.receive()); },
|
||||
"loopback receiver did not time out");
|
||||
require(!can_connect_ipv4(timed_port), "timed-out receiver left listener open");
|
||||
require_throws([&] { static_cast<void>(timed.receive()); },
|
||||
"timed-out receiver was reusable");
|
||||
|
||||
LoopbackCallbackReceiver partial{20ms};
|
||||
const unsigned int partial_port = port_from_uri(partial.redirect_uri());
|
||||
Descriptor partial_client = connect_ipv4(partial_port);
|
||||
send_bytes(partial_client.get(),
|
||||
"GET /nocal/oauth/callback?code=x&state=y HTTP/1.1\r\nHost: localhost:");
|
||||
require_throws([&] { static_cast<void>(partial.receive()); },
|
||||
"partial callback did not obey the total deadline");
|
||||
require_failure_response(read_response(partial_client.get()));
|
||||
require(!can_connect_ipv4(partial_port), "partial timeout left listener open");
|
||||
|
||||
LoopbackCallbackReceiver used{1s};
|
||||
const unsigned int used_port = port_from_uri(used.redirect_uri());
|
||||
static_cast<void>(valid_exchange(used, "code=x&state=y"));
|
||||
require_throws([&] { static_cast<void>(used.receive()); },
|
||||
"completed receiver was reusable");
|
||||
require(!can_connect_ipv4(used_port), "completed receiver left listener open");
|
||||
|
||||
unsigned int destroyed_port = 0;
|
||||
{
|
||||
LoopbackCallbackReceiver destroyed{1s};
|
||||
destroyed_port = port_from_uri(destroyed.redirect_uri());
|
||||
}
|
||||
require(!can_connect_ipv4(destroyed_port), "destroyed receiver left listener open");
|
||||
}
|
||||
|
||||
void test_move_safety_and_cleanup() {
|
||||
LoopbackCallbackReceiver source{1s};
|
||||
const std::string uri = source.redirect_uri();
|
||||
LoopbackCallbackReceiver moved{std::move(source)};
|
||||
require(moved.redirect_uri() == uri, "move construction changed redirect URI");
|
||||
require_throws([&] { static_cast<void>(source.redirect_uri()); },
|
||||
"moved-from receiver remained usable");
|
||||
static_cast<void>(valid_exchange(moved, "code=moved&state=state"));
|
||||
|
||||
LoopbackCallbackReceiver assignment_source{1s};
|
||||
const std::string assigned_uri = assignment_source.redirect_uri();
|
||||
LoopbackCallbackReceiver assignment_target{1s};
|
||||
const unsigned int replaced_port = port_from_uri(assignment_target.redirect_uri());
|
||||
assignment_target = std::move(assignment_source);
|
||||
require(assignment_target.redirect_uri() == assigned_uri,
|
||||
"move assignment changed redirect URI");
|
||||
require(!can_connect_ipv4(replaced_port),
|
||||
"move assignment did not close the replaced listener");
|
||||
require_throws([&] { static_cast<void>(assignment_source.redirect_uri()); },
|
||||
"move-assigned source remained usable");
|
||||
static_cast<void>(valid_exchange(assignment_target, "error=denied&state=state"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc >= 2 && std::string_view{argv[1]}.starts_with("browser-helper:")) {
|
||||
return run_browser_helper(argc, argv);
|
||||
}
|
||||
try {
|
||||
TempDirectory temporary;
|
||||
test_browser_literal_argv_and_failures(temporary.path());
|
||||
test_redirect_and_parsed_callbacks();
|
||||
test_invalid_http_matrix();
|
||||
test_timeout_single_use_and_cleanup();
|
||||
test_move_safety_and_cleanup();
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "desktop OAuth test failure: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
std::cout << "desktop OAuth tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user