2026-06-17 09:24:59 -04:00
|
|
|
// =============================================================================
|
|
|
|
|
// Tests for the host-side TokenSource reader.
|
|
|
|
|
//
|
|
|
|
|
// The module host reads its auth token from a channel its container designates
|
|
|
|
|
// via --token-source (stdin | fd:<n> | file:<path>), with no dependency on any
|
|
|
|
|
// container implementation. These tests exercise each source over real pipes
|
|
|
|
|
// and a real temp file, plus the timeout and error paths.
|
|
|
|
|
// =============================================================================
|
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
#include "token_source.h"
|
|
|
|
|
|
|
|
|
|
#include <fcntl.h>
|
|
|
|
|
#include <unistd.h>
|
2026-08-11 14:46:37 -03:00
|
|
|
#ifdef _WIN32
|
|
|
|
|
// Deliberately NOT <windows.h>: winnt.h declares a TOKEN_INFORMATION_CLASS
|
|
|
|
|
// enumerator literally named TokenSource, which collides head-on with this
|
|
|
|
|
// project's `namespace TokenSource` --
|
|
|
|
|
// winnt.h:4131: error: 'TokenSource' redeclared as different kind of entity
|
|
|
|
|
// Everything needed here lives in the CRT headers instead.
|
|
|
|
|
#include <io.h> // _pipe / _write / _mktemp_s
|
|
|
|
|
#include <share.h> // _SH_DENYNO
|
|
|
|
|
#include <sys/stat.h> // _S_IREAD / _S_IWRITE
|
|
|
|
|
#endif
|
2026-06-17 09:24:59 -04:00
|
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <thread>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
2026-08-11 14:46:37 -03:00
|
|
|
// mingw has no POSIX pipe(2): the CRT spells it _pipe and additionally wants a
|
|
|
|
|
// buffer size and a text/binary mode. _O_BINARY matters -- the token is
|
|
|
|
|
// compared byte for byte, and text mode would translate the "\r\n" the CRLF
|
|
|
|
|
// test deliberately writes.
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
inline int makePipe(int fds[2]) { return ::_pipe(fds, 4096, _O_BINARY); }
|
|
|
|
|
inline int writeFd(int fd, const void* buf, std::size_t n) {
|
|
|
|
|
return ::_write(fd, buf, static_cast<unsigned int>(n));
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
inline int makePipe(int fds[2]) { return ::pipe(fds); }
|
|
|
|
|
inline ssize_t writeFd(int fd, const void* buf, std::size_t n) {
|
|
|
|
|
return ::write(fd, buf, n);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// A temp file open for writing, plus its path. mingw has neither mkstemp nor
|
|
|
|
|
// a /tmp, so the Windows branch asks the OS for both. Returns -1 on failure.
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
inline int makeTempFile(std::string& pathOut) {
|
|
|
|
|
// _mktemp_s rewrites the trailing XXXXXX in place. Relative to the cwd,
|
|
|
|
|
// which is writable while the tests run -- and unlike the POSIX branch
|
|
|
|
|
// there is no /tmp to reach for.
|
|
|
|
|
char tmpl[] = "logos_token_src_XXXXXX";
|
|
|
|
|
if (::_mktemp_s(tmpl, sizeof tmpl) != 0) return -1;
|
|
|
|
|
int fd = -1;
|
|
|
|
|
if (::_sopen_s(&fd, tmpl, _O_RDWR | _O_CREAT | _O_EXCL | _O_BINARY,
|
|
|
|
|
_SH_DENYNO, _S_IREAD | _S_IWRITE) != 0)
|
|
|
|
|
return -1;
|
|
|
|
|
pathOut = tmpl;
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
inline int makeTempFile(std::string& pathOut) {
|
|
|
|
|
char path[] = "/tmp/logos_token_src_XXXXXX";
|
|
|
|
|
const int fd = ::mkstemp(path);
|
|
|
|
|
if (fd >= 0) pathOut = path;
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-06-17 09:24:59 -04:00
|
|
|
// Write `data` to a fresh pipe and return the read fd (caller closes it). The
|
|
|
|
|
// write end is closed after writing so the reader sees EOF.
|
|
|
|
|
int pipeWith(const std::string& data) {
|
|
|
|
|
int fds[2];
|
2026-08-11 14:46:37 -03:00
|
|
|
if (makePipe(fds) != 0) return -1;
|
2026-06-17 09:24:59 -04:00
|
|
|
ssize_t off = 0;
|
|
|
|
|
while (off < static_cast<ssize_t>(data.size())) {
|
2026-08-11 14:46:37 -03:00
|
|
|
auto n = writeFd(fds[1], data.data() + off, data.size() - off);
|
2026-06-17 09:24:59 -04:00
|
|
|
if (n <= 0) break;
|
|
|
|
|
off += n;
|
|
|
|
|
}
|
|
|
|
|
::close(fds[1]); // EOF for the reader
|
|
|
|
|
return fds[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
TEST(TokenSource, ReadsFromFdStripsNewline) {
|
|
|
|
|
const int rfd = pipeWith("my-secret-token\n");
|
|
|
|
|
ASSERT_GE(rfd, 0);
|
|
|
|
|
const std::string tok = TokenSource::read("fd:" + std::to_string(rfd));
|
|
|
|
|
::close(rfd);
|
|
|
|
|
EXPECT_EQ(tok, "my-secret-token");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(TokenSource, ReadsFromFdWithoutNewlineAtEof) {
|
|
|
|
|
const int rfd = pipeWith("no-newline-token");
|
|
|
|
|
ASSERT_GE(rfd, 0);
|
|
|
|
|
const std::string tok = TokenSource::read("fd:" + std::to_string(rfd));
|
|
|
|
|
::close(rfd);
|
|
|
|
|
EXPECT_EQ(tok, "no-newline-token");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(TokenSource, ReadsOnlyFirstLine) {
|
|
|
|
|
const int rfd = pipeWith("first-line\nsecond-line\n");
|
|
|
|
|
ASSERT_GE(rfd, 0);
|
|
|
|
|
const std::string tok = TokenSource::read("fd:" + std::to_string(rfd));
|
|
|
|
|
::close(rfd);
|
|
|
|
|
EXPECT_EQ(tok, "first-line");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(TokenSource, ReadsFromFile) {
|
2026-08-11 14:46:37 -03:00
|
|
|
std::string path;
|
|
|
|
|
const int fd = makeTempFile(path);
|
2026-06-17 09:24:59 -04:00
|
|
|
ASSERT_GE(fd, 0);
|
|
|
|
|
const char* contents = "file-token\n";
|
2026-08-11 14:46:37 -03:00
|
|
|
ASSERT_GT(writeFd(fd, contents, std::strlen(contents)), 0);
|
2026-06-17 09:24:59 -04:00
|
|
|
::close(fd);
|
|
|
|
|
|
|
|
|
|
const std::string tok = TokenSource::read(std::string("file:") + path);
|
2026-08-11 14:46:37 -03:00
|
|
|
std::remove(path.c_str());
|
2026-06-17 09:24:59 -04:00
|
|
|
EXPECT_EQ(tok, "file-token");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(TokenSource, UnknownSourceReturnsEmpty) {
|
|
|
|
|
EXPECT_TRUE(TokenSource::read("carrier-pigeon").empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(TokenSource, InvalidFdSpecReturnsEmpty) {
|
|
|
|
|
EXPECT_TRUE(TokenSource::read("fd:notanumber").empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(TokenSource, MissingFileReturnsEmpty) {
|
|
|
|
|
EXPECT_TRUE(TokenSource::read("file:/no/such/path/logos_token").empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(TokenSource, TimesOutWhenNoDataArrives) {
|
|
|
|
|
// A pipe whose write end stays open and silent: the read must hit the
|
|
|
|
|
// timeout and return empty rather than block forever.
|
|
|
|
|
int fds[2];
|
2026-08-11 14:46:37 -03:00
|
|
|
ASSERT_EQ(makePipe(fds), 0);
|
2026-06-17 09:24:59 -04:00
|
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
|
|
|
const std::string tok = TokenSource::read("fd:" + std::to_string(fds[0]),
|
|
|
|
|
/*timeout_ms=*/200);
|
|
|
|
|
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
std::chrono::steady_clock::now() - t0).count();
|
|
|
|
|
::close(fds[0]);
|
|
|
|
|
::close(fds[1]);
|
|
|
|
|
EXPECT_TRUE(tok.empty());
|
|
|
|
|
EXPECT_GE(elapsed, 150) << "should wait out the timeout, not return early";
|
|
|
|
|
EXPECT_LT(elapsed, 1500) << "should not block well past the timeout";
|
|
|
|
|
}
|