diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6ccfda56d..b12850aee 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -232,6 +232,7 @@ set(HTTP_TEST_SOURCES network/test_http_uri.cpp network/test_http_status_line.cpp network/test_http_parse_response_header.cpp + network/test_http_lifecycle.cpp ) add_executable(shadps4_http_test ${HTTP_TEST_SOURCES}) diff --git a/tests/network/test_http_lifecycle.cpp b/tests/network/test_http_lifecycle.cpp new file mode 100644 index 000000000..2cea01f51 --- /dev/null +++ b/tests/network/test_http_lifecycle.cpp @@ -0,0 +1,2103 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include + +#include "common/types.h" +#include "core/libraries/network/http.h" +#include "core/libraries/network/http_error.h" +#include "tests/stubs/kernel_stub.h" + +#ifndef ORBIS_OK +#define ORBIS_OK 0 +#endif + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpInit(int, int, u64); +int PS4_SYSV_ABI sceHttpTerm(int); +int PS4_SYSV_ABI sceHttpCreateTemplate(int, const char*, int, int); +int PS4_SYSV_ABI sceHttpDeleteTemplate(int); +int PS4_SYSV_ABI sceHttpCreateConnection(int, const char*, const char*, u16, int); +int PS4_SYSV_ABI sceHttpCreateConnectionWithURL(int, const char*, bool); +int PS4_SYSV_ABI sceHttpDeleteConnection(int); +int PS4_SYSV_ABI sceHttpCreateRequest(int, int, const char*, u64); +int PS4_SYSV_ABI sceHttpCreateRequestWithURL(int, s32, const char*, u64); +int PS4_SYSV_ABI sceHttpDeleteRequest(int); +int PS4_SYSV_ABI sceHttpSendRequest(int, const void*, u64); +int PS4_SYSV_ABI sceHttpGetLastErrno(int, int*); +int PS4_SYSV_ABI sceHttpGetStatusCode(int, int*); +int PS4_SYSV_ABI sceHttpReadData(s32, void*, u64); +int PS4_SYSV_ABI sceHttpAbortRequest(int); +int PS4_SYSV_ABI sceHttpGetAllResponseHeaders(int, char**, u64*); +} // namespace Libraries::Http + +using namespace Libraries::Http; + +namespace { + +// State machine + no-internet path.. SendRequest records ENODNS on the request and GetLastErrno +// surfaces it. + +// Drains every active context. Run before each test so previous test failures +// don't poison the static state. +static void DrainState() { + for (int i = 0; i < 1024; ++i) { + if (sceHttpTerm(i) != ORBIS_OK) { + // Either not active or library already torn down + } + } +} + +class HttpLifecycle : public ::testing::Test { +protected: + void SetUp() override { + DrainState(); + } + void TearDown() override { + DrainState(); + } +}; + +// Init returns a strictly-positive context id. +TEST_F(HttpLifecycle, InitReturnsContextId) { + int ctx = sceHttpInit(1, 2, 1024 * 16); + EXPECT_GT(ctx, 0); + EXPECT_EQ(sceHttpTerm(ctx), ORBIS_OK); +} + +// Init with poolSize=0 returns EINVAL. +TEST_F(HttpLifecycle, InitZeroPoolReturnsEinval) { + EXPECT_EQ(sceHttpInit(0, 0, 0), static_cast(ORBIS_KERNEL_ERROR_EINVAL)); +} + +// Term before Init returns BEFORE_INIT. +TEST_F(HttpLifecycle, TermBeforeInitFails) { + EXPECT_EQ(sceHttpTerm(1), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// Two Inits, two Terms both context IDs are different, both terminate. +TEST_F(HttpLifecycle, MultipleContexts) { + int a = sceHttpInit(0, 0, 4096); + int b = sceHttpInit(0, 0, 4096); + EXPECT_GT(a, 0); + EXPECT_GT(b, 0); + EXPECT_NE(a, b); + EXPECT_EQ(sceHttpTerm(a), ORBIS_OK); + EXPECT_EQ(sceHttpTerm(b), ORBIS_OK); +} + +// Term of an already-terminated context returns INVALID_ID, not BEFORE_INIT. +TEST_F(HttpLifecycle, TermTwiceReturnsInvalidId) { + int a = sceHttpInit(0, 0, 4096); + int b = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpTerm(a), ORBIS_OK); + // The library is still inited via b; another Term of a should now fail + // with INVALID_ID. + EXPECT_EQ(sceHttpTerm(a), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + EXPECT_EQ(sceHttpTerm(b), ORBIS_OK); +} + +// CreateTemplate before Init should return BEFORE_INIT. +TEST_F(HttpLifecycle, CreateTemplateBeforeInit) { + EXPECT_EQ(sceHttpCreateTemplate(1, "UA/1.0", 1, 0), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// CreateTemplate with bogus context id should return INVALID_ID. +TEST_F(HttpLifecycle, CreateTemplateInvalidCtx) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpCreateTemplate(ctx + 99, "UA/1.0", 1, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + EXPECT_EQ(sceHttpTerm(ctx), ORBIS_OK); +} + +// Sequence : Init then CreateTemplate then CreateConnection then CreateRequest then SendRequest +// then GetLastErrno reports ENODNS then Delete chain succeeds. +TEST_F(HttpLifecycle, FullLifecycleSurfacesNoInternetError) { + int ctx = sceHttpInit(0, 0, 4096); + ASSERT_GT(ctx, 0); + + int tmpl = sceHttpCreateTemplate(ctx, "UA/1.0", 1, 0); + ASSERT_GT(tmpl, 0); + EXPECT_NE(tmpl, ctx); // different ID space + + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + ASSERT_GT(conn, 0); + + int req = sceHttpCreateRequestWithURL(conn, 0, "http://example.com/", 0); + ASSERT_GT(req, 0); + + // Send dispatches a worker thread. Returns ORBIS_OK synchronously. + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + + // A blocking getter (GetStatusCode) waits on the worker's cv. After it + // returns, the worker has finished and last_errno is populated. On the + // no-internet path the transport failure makes GetStatusCode return + // BEFORE_SEND (no status line was ever received). + int sc = 0; + EXPECT_EQ(sceHttpGetStatusCode(req, &sc), static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + + // After the worker is done, GetLastErrno reports the resolver error. + int err = 0; + EXPECT_EQ(sceHttpGetLastErrno(req, &err), ORBIS_OK); + EXPECT_EQ(static_cast(err), 0x80436002u); + + EXPECT_EQ(sceHttpDeleteRequest(req), ORBIS_OK); + EXPECT_EQ(sceHttpDeleteConnection(conn), ORBIS_OK); + EXPECT_EQ(sceHttpDeleteTemplate(tmpl), ORBIS_OK); + EXPECT_EQ(sceHttpTerm(ctx), ORBIS_OK); +} + +// SendRequest twice on the same reqId: first dispatches the worker, second +// is rejected with AFTER_SEND (state is already Sending or Sent). +TEST_F(HttpLifecycle, SendTwiceReturnsAfterSend) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), static_cast(ORBIS_HTTP_ERROR_AFTER_SEND)); + // Drain the worker so the next test starts clean. + int sc; + sceHttpGetStatusCode(req, &sc); + sceHttpDeleteRequest(req); + sceHttpDeleteConnection(conn); + sceHttpDeleteTemplate(tmpl); + sceHttpTerm(ctx); +} + +// SendRequest on bogus id shoud return INVALID_ID. +TEST_F(HttpLifecycle, SendInvalidRequest) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSendRequest(9999, nullptr, 0), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// GetLastErrno before send returns 0 (the default, no error recorded yet). +TEST_F(HttpLifecycle, GetLastErrnoBeforeSendIsZero) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + int err = 0xDEADBEEF; + EXPECT_EQ(sceHttpGetLastErrno(req, &err), ORBIS_OK); + EXPECT_EQ(err, 0); + sceHttpDeleteRequest(req); + sceHttpDeleteConnection(conn); + sceHttpDeleteTemplate(tmpl); + sceHttpTerm(ctx); +} + +// GetLastErrno with null output pointer should return INVALID_VALUE. +TEST_F(HttpLifecycle, GetLastErrnoNullOut) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpGetLastErrno(req, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// Term tears down all dependent objects: a request id from a terminated +// context is no longer valid. +TEST_F(HttpLifecycle, TermTearsDownDependents) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpTerm(ctx), ORBIS_OK); + + // Operations after term must report BEFORE_INIT. + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + int err; + EXPECT_EQ(sceHttpGetLastErrno(req, &err), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// Connection with null server name should return INVALID_VALUE. +TEST_F(HttpLifecycle, CreateConnectionNullServerName) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpCreateConnection(tmpl, nullptr, "http", 80, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// Two requests on the same connection get distinct IDs and isolated errnos. +TEST_F(HttpLifecycle, TwoRequestsAreIsolated) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int r1 = sceHttpCreateRequestWithURL(conn, 0, "http://x/a", 0); + int r2 = sceHttpCreateRequestWithURL(conn, 0, "http://x/b", 0); + EXPECT_NE(r1, r2); + + EXPECT_EQ(sceHttpSendRequest(r1, nullptr, 0), ORBIS_OK); + // Wait for r1's worker via a blocking getter (GetStatusCode). + int sc; + sceHttpGetStatusCode(r1, &sc); + // r2 not yet sent + int err1 = 0, err2 = 0; + EXPECT_EQ(sceHttpGetLastErrno(r1, &err1), ORBIS_OK); + EXPECT_EQ(sceHttpGetLastErrno(r2, &err2), ORBIS_OK); + EXPECT_EQ(static_cast(err1), 0x80436002u); + EXPECT_EQ(err2, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateConnectionNullServerNameTakesPriorityOverBadScheme) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpCreateConnection(tmpl, nullptr, "ftp", 80, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateConnectionUnknownScheme) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpCreateConnection(tmpl, "example.com", "ftp", 80, 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_SCHEME)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateConnectionPortZeroAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_GT(sceHttpCreateConnection(tmpl, "example.com", "http", 0, 0), 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestRejectsOptions) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + EXPECT_EQ(sceHttpCreateRequestWithURL(conn, /*Options=*/3, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_METHOD)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestRejectsConnect) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + EXPECT_EQ(sceHttpCreateRequestWithURL(conn, /*Connect=*/7, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_METHOD)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestRejectsOutOfRangeMethod) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + EXPECT_EQ(sceHttpCreateRequestWithURL(conn, 9, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_METHOD)); + EXPECT_EQ(sceHttpCreateRequestWithURL(conn, 99, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_METHOD)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestAcceptsValidMethods) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + for (int m : {0, 1, 2, 4, 5, 6, 8}) { + int r = sceHttpCreateRequestWithURL(conn, m, "http://example.com/", 0); + EXPECT_GT(r, 0) << "method " << m << " unexpectedly rejected"; + if (r > 0) + sceHttpDeleteRequest(r); + } + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpCreateRequestWithURL2(int, const char*, const char*, u64); +} + +namespace { + +// Verifies the WithURL2 ordering +TEST_F(HttpLifecycle, WithURL2BeforeInitTakesPriorityOverNullMethod) { + // Library uninitialized (DrainState ran via SetUp). + EXPECT_EQ(sceHttpCreateRequestWithURL2(/*connId=*/1, nullptr, "http://x/", 0), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// Initialized but bogus connId + NULL method should return INVALID_ID +TEST_F(HttpLifecycle, WithURL2InvalidIdTakesPriorityOverNullMethod) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpCreateRequestWithURL2(/*connId=*/9999, nullptr, "http://x/", 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// Initialized + valid connId + NULL method should return INVALID_VALUE +TEST_F(HttpLifecycle, WithURL2NullMethodAfterValidConn) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + EXPECT_EQ(sceHttpCreateRequestWithURL2(conn, nullptr, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpSetAutoRedirect(int id, int isEnable); +int PS4_SYSV_ABI sceHttpGetAutoRedirect(int id, int* isEnable); +int PS4_SYSV_ABI sceHttpSetConnectTimeOut(int id, u32 usec); +} // namespace Libraries::Http + +namespace { + +// Connection snapshots template settings at creation. +TEST_F(HttpLifecycle, ConnectionSnapshotsTemplateAutoRedirectAtCreation) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + + // Flip the template's default (which is true) to false. + EXPECT_EQ(sceHttpSetAutoRedirect(tmpl, 0), ORBIS_OK); + + // Connection created NOW should snapshot redirect=false. + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + ASSERT_GT(conn, 0); + + // Flip the template AGAIN to true. The connection should NOT change. + EXPECT_EQ(sceHttpSetAutoRedirect(tmpl, 1), ORBIS_OK); + + int got = 99; + EXPECT_EQ(sceHttpGetAutoRedirect(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 0) << "connection should keep snapshot value, not follow template"; + + // And the template's current value is now 1. + EXPECT_EQ(sceHttpGetAutoRedirect(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + + sceHttpTerm(ctx); +} + +// Request snapshots connection settings at creation. +TEST_F(HttpLifecycle, RequestSnapshotsConnectionSettingsAtCreation) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + ASSERT_GT(conn, 0); + + // Default auto_redirect on conn is true (inherited from tmpl default). + int got = -1; + EXPECT_EQ(sceHttpGetAutoRedirect(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + + // Flip the connection to false BEFORE creating the request. + EXPECT_EQ(sceHttpSetAutoRedirect(conn, 0), ORBIS_OK); + + int req = sceHttpCreateRequestWithURL(conn, /*method=*/0, "http://example.com/", 0); + ASSERT_GT(req, 0); + + // Now flip the connection back to true. Should NOT affect the request. + EXPECT_EQ(sceHttpSetAutoRedirect(conn, 1), ORBIS_OK); + + got = 99; + EXPECT_EQ(sceHttpGetAutoRedirect(req, &got), ORBIS_OK); + EXPECT_EQ(got, 0) << "request should keep snapshot, not follow connection"; + + // Connection's current value is 1. + EXPECT_EQ(sceHttpGetAutoRedirect(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + + sceHttpTerm(ctx); +} + +// Default chain: template default then connection default then request default +// should all be auto_redirect=true +TEST_F(HttpLifecycle, AutoRedirectDefaultsPropagateThroughCreation) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://example.com/", 0); + + int got = -1; + EXPECT_EQ(sceHttpGetAutoRedirect(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + EXPECT_EQ(sceHttpGetAutoRedirect(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + EXPECT_EQ(sceHttpGetAutoRedirect(req, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + + sceHttpTerm(ctx); +} + +// Timeouts also snapshot correctly +TEST_F(HttpLifecycle, ConnectTimeoutSnapshotsTemplateValue) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + + EXPECT_EQ(sceHttpSetConnectTimeOut(tmpl, 7'000'000), ORBIS_OK); + + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + ASSERT_GT(conn, 0); + + EXPECT_EQ(sceHttpSetConnectTimeOut(tmpl, 99'000'000), ORBIS_OK); + + sceHttpTerm(ctx); +} + +} // namespace + +// Async state machine sanity tests +namespace { + +// After SendRequest, a blocking ReadData waits for the worker, then returns 0 +TEST_F(HttpLifecycle, ReadDataReturnsZeroAfterTransportFailure) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + char buf[16]; + EXPECT_EQ(sceHttpReadData(req, buf, sizeof(buf)), 0); + sceHttpTerm(ctx); +} + +// ReadData on an unsent request should return BEFORE_SEND (Created state). +TEST_F(HttpLifecycle, ReadDataBeforeSendReturnsBeforeSend) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + char buf[16]; + EXPECT_EQ(sceHttpReadData(req, buf, sizeof(buf)), + static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + sceHttpTerm(ctx); +} + +// GetStatusCode on an aborted request should return ABORTED. +TEST_F(HttpLifecycle, GetStatusCodeOnAbortedRequest) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + int sc; + EXPECT_EQ(sceHttpGetStatusCode(req, &sc), static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +// AbortRequest on unsent request: state goes to Aborted, subsequent Send +// returns ABORTED. +TEST_F(HttpLifecycle, AbortBeforeSend) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace { + +TEST_F(HttpLifecycle, GetAllResponseHeadersEmptyOnTransportFailure) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + char* hdr = reinterpret_cast(0xdeadbeef); + u64 hdr_size = 999; + EXPECT_EQ(sceHttpGetAllResponseHeaders(req, &hdr, &hdr_size), ORBIS_OK); + EXPECT_EQ(hdr, nullptr); + EXPECT_EQ(hdr_size, 0u); + sceHttpTerm(ctx); +} + +// GetAllResponseHeaders before Send should return BEFORE_SEND. +TEST_F(HttpLifecycle, GetAllResponseHeadersBeforeSend) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + char* hdr = nullptr; + u64 hdr_size = 0; + EXPECT_EQ(sceHttpGetAllResponseHeaders(req, &hdr, &hdr_size), + static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + sceHttpTerm(ctx); +} + +// Null output pointer should return INVALID_VALUE (init OK, no reqId lookup). +TEST_F(HttpLifecycle, GetAllResponseHeadersNullOut) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpGetAllResponseHeaders(0, nullptr, nullptr), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace { + +TEST_F(HttpLifecycle, AbortAfterSendIsIdempotent) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + int sc; + EXPECT_EQ(sceHttpGetStatusCode(req, &sc), static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + EXPECT_EQ(sceHttpGetStatusCode(req, &sc), static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + int err = 0; + EXPECT_EQ(sceHttpGetLastErrno(req, &err), ORBIS_OK); + EXPECT_EQ(static_cast(err), 0x80436002u); + + sceHttpTerm(ctx); +} + +// Aborting twice on the same request is a no-op the second time. +TEST_F(HttpLifecycle, AbortTwiceIsIdempotent) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + // Second call: idempotent OK. + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + + int sc; + EXPECT_EQ(sceHttpGetStatusCode(req, &sc), static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpSetNonblock(int, int); +int PS4_SYSV_ABI sceHttpGetNonblock(int, int*); +int PS4_SYSV_ABI sceHttpTrySetNonblock(int, int); +int PS4_SYSV_ABI sceHttpTryGetNonblock(int, int*); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, GetNonblockDefaultIsBlocking) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + EXPECT_EQ(sceHttpGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 0); + sceHttpTerm(ctx); +} + +// Set + Get round-trip. +TEST_F(HttpLifecycle, SetGetNonblockRoundTrip) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + EXPECT_EQ(sceHttpSetNonblock(tmpl, 1), ORBIS_OK); + EXPECT_EQ(sceHttpGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + EXPECT_EQ(sceHttpSetNonblock(tmpl, 0), ORBIS_OK); + EXPECT_EQ(sceHttpGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 0); + sceHttpTerm(ctx); +} + +// Try variants delegate to the regular ones. +TEST_F(HttpLifecycle, TryNonblockBehavesAsRegular) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + EXPECT_EQ(sceHttpTrySetNonblock(tmpl, 1), ORBIS_OK); + EXPECT_EQ(sceHttpTryGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + sceHttpTerm(ctx); +} + +// Nonblock accepts template/connection/request IDs. +TEST_F(HttpLifecycle, SetNonblockAtRequestLevel) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSetNonblock(req, 1), ORBIS_OK); + int got = -1; + EXPECT_EQ(sceHttpGetNonblock(req, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, NonblockSnapshotsAtCreation) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetNonblock(tmpl, 1), ORBIS_OK); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + // Conn should have snapshot value = 1. + int got = -1; + EXPECT_EQ(sceHttpGetNonblock(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + // Flip template to 0; conn keeps its snapshot of 1. + EXPECT_EQ(sceHttpSetNonblock(tmpl, 0), ORBIS_OK); + EXPECT_EQ(sceHttpGetNonblock(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + EXPECT_EQ(sceHttpGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, GetNonblockInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + int got = -1; + EXPECT_EQ(sceHttpGetNonblock(99999, &got), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// NULL output pointer on Get should return INVALID_VALUE. +TEST_F(HttpLifecycle, GetNonblockNullOut) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpGetNonblock(tmpl, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpGetResponseContentLength(int, int*, u64*); +} + +namespace { + +TEST_F(HttpLifecycle, NonblockReadDataAfterCompletionReturnsZero) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + // Drain via blocking GetStatusCode (default blocking mode). + int sc; + sceHttpGetStatusCode(req, &sc); + // Flip to nonblock - worker is done, nonblock check no longer fires. + sceHttpSetNonblock(req, 1); + char buf[16]; + EXPECT_EQ(sceHttpReadData(req, buf, sizeof(buf)), 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, NonblockGetStatusCodeAfterCompletionWorks) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + // Drain via blocking GetStatusCode first (state Sent). + int sc; + sceHttpGetStatusCode(req, &sc); + // Flip to nonblock now; worker is done. + sceHttpSetNonblock(req, 1); + int r = sceHttpGetStatusCode(req, &sc); + // For a transport-failed request, GetStatusCode returns BEFORE_SEND + // (because last_errno != 0 and status_code == 0). + EXPECT_EQ(r, static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, NonblockGetResponseContentLengthAfterCompletion) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + // Drain worker via blocking GetStatusCode (request is still in default + // blocking mode). + int sc; + sceHttpGetStatusCode(req, &sc); + // Now flip to nonblock. The worker is done; subsequent getters should + // succeed (nonblock only affects the Sending state). + sceHttpSetNonblock(req, 1); + int result = -999; + u64 content_length = 999; + int r = sceHttpGetResponseContentLength(req, &result, &content_length); + EXPECT_EQ(r, ORBIS_OK); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpCreateRequest2(int, const char*, const char*, u64); +int PS4_SYSV_ABI sceHttpCreateRequestWithURL2(int, const char*, const char*, u64); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, CreateRequest2DoesNotDeadlock) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequest2(conn, "GET", "/path", 0); + EXPECT_GT(req, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestWithURL2DoesNotDeadlock) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequestWithURL2(conn, "POST", "http://example.com/post", 0); + EXPECT_GT(req, 0); + sceHttpTerm(ctx); +} + +// CreateRequest2 with non-standard method routes via CUSTOM slot. +TEST_F(HttpLifecycle, CreateRequest2CustomMethodWorks) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequest2(conn, "PATCH", "/resource", 0); + EXPECT_GT(req, 0); + sceHttpTerm(ctx); +} + +// CreateRequest2 NULL method should return INVALID_VALUE +TEST_F(HttpLifecycle, CreateRequest2NullMethod) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequest2(conn, nullptr, "/path", 0); + EXPECT_EQ(req, static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// CreateRequest2 NULL path should return INVALID_VALUE. +TEST_F(HttpLifecycle, CreateRequest2NullPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequest2(conn, "GET", nullptr, 0); + EXPECT_EQ(req, static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +// Epoll lifecycle tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpCreateEpoll(int, OrbisHttpEpollHandle*); +int PS4_SYSV_ABI sceHttpDestroyEpoll(int, OrbisHttpEpollHandle); +int PS4_SYSV_ABI sceHttpSetEpoll(int, OrbisHttpEpollHandle, void*); +int PS4_SYSV_ABI sceHttpGetEpoll(int, OrbisHttpEpollHandle*, void**); +int PS4_SYSV_ABI sceHttpUnsetEpoll(int); +int PS4_SYSV_ABI sceHttpWaitRequest(OrbisHttpEpollHandle, OrbisHttpNBEvent*, int, int); +int PS4_SYSV_ABI sceHttpAbortWaitRequest(OrbisHttpEpollHandle); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, CreateAndDestroyEpoll) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + EXPECT_EQ(sceHttpCreateEpoll(ctx, &eh), ORBIS_OK); + EXPECT_NE(eh, nullptr); + EXPECT_EQ(sceHttpDestroyEpoll(ctx, eh), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateEpollNullOut) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpCreateEpoll(ctx, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateEpollInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + EXPECT_EQ(sceHttpCreateEpoll(99999, &eh), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, DestroyEpollNullHandle) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpDestroyEpoll(ctx, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, DestroyEpollInvalidHandle) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle bogus = reinterpret_cast(uintptr_t{99999}); + EXPECT_EQ(sceHttpDestroyEpoll(ctx, bogus), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetGetEpollRoundTripOnTemplate) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + int magic = 0; + void* user_arg = &magic; + EXPECT_EQ(sceHttpSetEpoll(tmpl, eh, user_arg), ORBIS_OK); + + OrbisHttpEpollHandle got_eh = nullptr; + void* got_user_arg = nullptr; + EXPECT_EQ(sceHttpGetEpoll(tmpl, &got_eh, &got_user_arg), ORBIS_OK); + EXPECT_EQ(got_eh, eh); + EXPECT_EQ(got_user_arg, user_arg); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetEpollInvalidEpollId) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle bogus = reinterpret_cast(uintptr_t{99999}); + EXPECT_EQ(sceHttpSetEpoll(tmpl, bogus, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetEpollInvalidOwnerId) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + EXPECT_EQ(sceHttpSetEpoll(99999, eh, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, EpollBindingSnapshotsFromTemplateToConnectionToRequest) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + int magic = 0; + EXPECT_EQ(sceHttpSetEpoll(tmpl, eh, &magic), ORBIS_OK); + + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + OrbisHttpEpollHandle got_eh = nullptr; + void* got_user_arg = nullptr; + EXPECT_EQ(sceHttpGetEpoll(conn, &got_eh, &got_user_arg), ORBIS_OK); + EXPECT_EQ(got_eh, eh); + EXPECT_EQ(got_user_arg, &magic); + + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpGetEpoll(req, &got_eh, &got_user_arg), ORBIS_OK); + EXPECT_EQ(got_eh, eh); + EXPECT_EQ(got_user_arg, &magic); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, UnsetEpollIsRequestOnly) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpUnsetEpoll(tmpl), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + EXPECT_EQ(sceHttpUnsetEpoll(conn), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + // Request id works. + EXPECT_EQ(sceHttpUnsetEpoll(req), ORBIS_OK); + // After unset, GetEpoll returns 0 / nullptr. + OrbisHttpEpollHandle got_eh = reinterpret_cast(uintptr_t{0xdead}); + void* got_user_arg = reinterpret_cast(uintptr_t{0xdead}); + EXPECT_EQ(sceHttpGetEpoll(req, &got_eh, &got_user_arg), ORBIS_OK); + EXPECT_EQ(got_eh, nullptr); + EXPECT_EQ(got_user_arg, nullptr); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestInvalidArgs) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + OrbisHttpNBEvent ev{}; + // maxevents <= 0 + EXPECT_EQ(sceHttpWaitRequest(eh, &ev, 0, 0), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpWaitRequest(eh, nullptr, 1, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpWaitRequest(nullptr, &ev, 1, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestPollEmptyReturnsZero) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + OrbisHttpNBEvent ev{}; + EXPECT_EQ(sceHttpWaitRequest(eh, &ev, 1, 0), 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestDrainsWorkerCompletionEvent) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + int magic = 0; + sceHttpSetEpoll(tmpl, eh, &magic); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + // Drain via blocking getter so the worker is guaranteed to have run. + int sc; + sceHttpGetStatusCode(req, &sc); + // Now drain the epoll queue. + OrbisHttpNBEvent ev{}; + int count = sceHttpWaitRequest(eh, &ev, 1, 0); + EXPECT_EQ(count, 1); + EXPECT_EQ(ev.id, req); + EXPECT_EQ(ev.userArg, &magic); + // No-internet path: failure bits (SOCK_ERR | HUP) are present. + EXPECT_NE(ev.events & 0x00020010u, 0u); // RESOLVER_ERR (0x20000) or HUP (0x10) + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AbortWaitRequestNullHandle) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpAbortWaitRequest(nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AbortWaitRequestInvalidHandle) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle bogus = reinterpret_cast(uintptr_t{99999}); + EXPECT_EQ(sceHttpAbortWaitRequest(bogus), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, TermClearsEpolls) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + sceHttpTerm(ctx); + // After Term, using the epoll handle returns BEFORE_INIT. + OrbisHttpNBEvent ev{}; + EXPECT_EQ(sceHttpWaitRequest(eh, &ev, 1, 0), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, AllEpollCallsBeforeInitReturnBeforeInit) { + OrbisHttpEpollHandle eh = nullptr; + OrbisHttpEpollHandle stub = reinterpret_cast(uintptr_t{1}); + OrbisHttpNBEvent ev{}; + void* ua = nullptr; + EXPECT_EQ(sceHttpCreateEpoll(0, &eh), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpDestroyEpoll(0, stub), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpSetEpoll(0, stub, nullptr), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpGetEpoll(0, &eh, &ua), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpUnsetEpoll(0), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpWaitRequest(stub, &ev, 1, 0), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpAbortWaitRequest(stub), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +} // namespace + +namespace { + +TEST_F(HttpLifecycle, CreateEpollBadCtxAndNullOutReturnsInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpCreateEpoll(99999, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// DestroyEpoll: same firmware order. +TEST_F(HttpLifecycle, DestroyEpollBadCtxAndNullEhReturnsInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpDestroyEpoll(99999, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestAfterAbortReturnsAborted) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + EXPECT_EQ(sceHttpAbortWaitRequest(eh), ORBIS_OK); + OrbisHttpNBEvent ev{}; + int r = sceHttpWaitRequest(eh, &ev, 1, 0); + EXPECT_EQ(r, static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestAbortedTakesPriorityOverQueuedEvents) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + sceHttpSetEpoll(tmpl, eh, nullptr); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + int sc; + sceHttpGetStatusCode(req, &sc); // drain worker (pushes a failure event) + // Abort first, then call WaitRequest. + sceHttpAbortWaitRequest(eh); + OrbisHttpNBEvent ev{}; + int r = sceHttpWaitRequest(eh, &ev, 1, 0); + EXPECT_EQ(r, static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +} // namespace + +// sceHttpAddRequestHeader tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpAddRequestHeader(int, const char*, const char*, s32); +} + +namespace { + +// Happy path: add a header on a template id, no errors. +TEST_F(HttpLifecycle, AddRequestHeaderTemplateHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "Content-Type", "application/json", 1), ORBIS_OK); + sceHttpTerm(ctx); +} + +// Headers can be added on connection ids too. +TEST_F(HttpLifecycle, AddRequestHeaderConnectionIdAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + EXPECT_EQ(sceHttpAddRequestHeader(conn, "X-Custom", "foo", 1), ORBIS_OK); + sceHttpTerm(ctx); +} + +// And on request ids. +TEST_F(HttpLifecycle, AddRequestHeaderRequestIdAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpAddRequestHeader(req, "X-Custom", "bar", 1), ORBIS_OK); + sceHttpTerm(ctx); +} + +// BEFORE_INIT when library not inited. +TEST_F(HttpLifecycle, AddRequestHeaderBeforeInit) { + EXPECT_EQ(sceHttpAddRequestHeader(1, "Foo", "bar", 0), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// INVALID_VALUE for mode out of range. +TEST_F(HttpLifecycle, AddRequestHeaderInvalidMode) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "Foo", "bar", 2), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "Foo", "bar", -1), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// INVALID_VALUE for null name. +TEST_F(HttpLifecycle, AddRequestHeaderNullName) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, nullptr, "bar", 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// INVALID_ID for non-existent id. +TEST_F(HttpLifecycle, AddRequestHeaderInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpAddRequestHeader(99999, "Foo", "bar", 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AddRequestHeaderModeCheckedBeforeId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpAddRequestHeader(99999, "Foo", "bar", 2), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AddRequestHeaderNullValue) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "X-Empty", nullptr, 1), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +// sceHttpRemoveRequestHeader tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpRemoveRequestHeader(int, const char*); +} + +namespace { + +// Add then remove: succeeds. +TEST_F(HttpLifecycle, RemoveRequestHeaderHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "X-Foo", "bar", 1), ORBIS_OK); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Foo"), ORBIS_OK); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Foo"), + static_cast(ORBIS_HTTP_ERROR_NOT_FOUND)); + sceHttpTerm(ctx); +} + +// Remove when the header was never added should return NOT_FOUND. +TEST_F(HttpLifecycle, RemoveRequestHeaderNotPresent) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "Nonexistent"), + static_cast(ORBIS_HTTP_ERROR_NOT_FOUND)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, RemoveRequestHeaderRemovesAllDuplicates) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + sceHttpAddRequestHeader(tmpl, "X-Dup", "v1", 1); + sceHttpAddRequestHeader(tmpl, "X-Dup", "v2", 1); + sceHttpAddRequestHeader(tmpl, "X-Dup", "v3", 1); + sceHttpAddRequestHeader(tmpl, "X-Other", "keep", 1); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Dup"), ORBIS_OK); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Dup"), + static_cast(ORBIS_HTTP_ERROR_NOT_FOUND)); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Other"), ORBIS_OK); + sceHttpTerm(ctx); +} + +// Case-insensitive match per HTTP semantics. +TEST_F(HttpLifecycle, RemoveRequestHeaderCaseInsensitive) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + sceHttpAddRequestHeader(tmpl, "Content-Type", "application/json", 1); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "content-type"), ORBIS_OK); + sceHttpTerm(ctx); +} + +// BEFORE_INIT when library not inited. +TEST_F(HttpLifecycle, RemoveRequestHeaderBeforeInit) { + EXPECT_EQ(sceHttpRemoveRequestHeader(1, "X-Foo"), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// Null nameshould return NOT_FOUND +TEST_F(HttpLifecycle, RemoveRequestHeaderNullNameReturnsNotFound) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, nullptr), + static_cast(ORBIS_HTTP_ERROR_NOT_FOUND)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, RemoveRequestHeaderInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpRemoveRequestHeader(99999, "X-Foo"), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// Works on connection ids too. +TEST_F(HttpLifecycle, RemoveRequestHeaderConnectionIdAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + sceHttpAddRequestHeader(conn, "X-Custom", "foo", 1); + EXPECT_EQ(sceHttpRemoveRequestHeader(conn, "X-Custom"), ORBIS_OK); + sceHttpTerm(ctx); +} + +// And request ids. +TEST_F(HttpLifecycle, RemoveRequestHeaderRequestIdAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + sceHttpAddRequestHeader(req, "X-Custom", "bar", 1); + EXPECT_EQ(sceHttpRemoveRequestHeader(req, "X-Custom"), ORBIS_OK); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpSetAcceptEncodingGZIPEnabled(int, int); +int PS4_SYSV_ABI sceHttpGetAcceptEncodingGZIPEnabled(int, int*); +int PS4_SYSV_ABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int, int); +int PS4_SYSV_ABI sceHttpSetResolveTimeOut(int, u32); +int PS4_SYSV_ABI sceHttpSetResolveRetry(int, int); +int PS4_SYSV_ABI sceHttpSetRecvBlockSize(int, u32); +int PS4_SYSV_ABI sceHttpSetResponseHeaderMaxSize(int, u64); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, AcceptEncodingGZIPDefaultIsTrue) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + EXPECT_EQ(sceHttpGetAcceptEncodingGZIPEnabled(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AcceptEncodingGZIPRoundTrip) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetAcceptEncodingGZIPEnabled(tmpl, 0), ORBIS_OK); + int got = -1; + EXPECT_EQ(sceHttpGetAcceptEncodingGZIPEnabled(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 0); + EXPECT_EQ(sceHttpSetAcceptEncodingGZIPEnabled(tmpl, 1), ORBIS_OK); + EXPECT_EQ(sceHttpGetAcceptEncodingGZIPEnabled(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AcceptEncodingGZIPGetNullPtr) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpGetAcceptEncodingGZIPEnabled(tmpl, nullptr), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AcceptEncodingGZIPSnapshotsToConnection) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + sceHttpSetAcceptEncodingGZIPEnabled(tmpl, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int got = -1; + sceHttpGetAcceptEncodingGZIPEnabled(conn, &got); + EXPECT_EQ(got, 0); // inherits from template + sceHttpTerm(ctx); +} + +// --- SetDefault: library global affects future templates --- + +TEST_F(HttpLifecycle, SetDefaultAcceptEncodingGZIPAffectsNewTemplates) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetDefaultAcceptEncodingGZIPEnabled(ctx, 0), ORBIS_OK); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + sceHttpGetAcceptEncodingGZIPEnabled(tmpl, &got); + EXPECT_EQ(got, 0); // picked up the new default + // Flip back so following tests aren't affected by global state. + sceHttpSetDefaultAcceptEncodingGZIPEnabled(ctx, 1); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetDefaultAcceptEncodingGZIPBeforeInit) { + EXPECT_EQ(sceHttpSetDefaultAcceptEncodingGZIPEnabled(1, 1), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// --- ResolveTimeOut validation --- + +TEST_F(HttpLifecycle, SetResolveTimeOutHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // SDK >= 1.70 requires usec > 999999 per firmware line 16020. + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 5000000u), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveTimeOutMinAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // 1000000 is one above the firmware threshold (must be > 999999). + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 1000000u), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveTimeOutSmallRejected) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // 999999 fails the strict (usec > 999999) check on SDK >= 1.70. + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 500000u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveTimeOutBeforeInit) { + EXPECT_EQ(sceHttpSetResolveTimeOut(1, 100), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, SetResolveTimeOutOutOfRangeBeforeBadId) { + int ctx = sceHttpInit(0, 0, 4096); + // usec=100 fails the > 999999 check on SDK >= 1.70 before id is looked up. + EXPECT_EQ(sceHttpSetResolveTimeOut(99999, 100u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveTimeOut_PreSDK170_AcceptsSmallUsec) { + Libraries::Kernel::TestSetSdkVersion(0x1600000); + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 100u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 0u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::TestResetSdkVersion(); +} + +TEST_F(HttpLifecycle, SetResolveTimeOut_AtSDK170_AppliesStrictCheck) { + Libraries::Kernel::TestSetSdkVersion(0x1700000); + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 1000000u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::TestResetSdkVersion(); +} + +TEST_F(HttpLifecycle, SetResolveTimeOut_PreSDK170_OutOfRangeStillAccepted) { + Libraries::Kernel::TestSetSdkVersion(0x1600000); + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 500000u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::TestResetSdkVersion(); +} + +// --- ResolveRetry --- + +TEST_F(HttpLifecycle, SetResolveRetryHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveRetry(tmpl, 0), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveRetry(tmpl, 3), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveRetry(tmpl, 100), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveRetryNegativeRejected) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveRetry(tmpl, -1), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// Firmware ordering: retry < 0 checked BEFORE id lookup. +TEST_F(HttpLifecycle, SetResolveRetryNegativeBeforeBadId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetResolveRetry(99999, -5), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// --- RecvBlockSize --- + +TEST_F(HttpLifecycle, SetRecvBlockSizeHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetRecvBlockSize(tmpl, 16384), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetRecvBlockSizeInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetRecvBlockSize(99999, 4096), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// --- ResponseHeaderMaxSize --- + +TEST_F(HttpLifecycle, SetResponseHeaderMaxSizeHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResponseHeaderMaxSize(tmpl, 8192), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResponseHeaderMaxSizeInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetResponseHeaderMaxSize(99999, 8192), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +} // namespace + +TEST_F(HttpLifecycle, SetResolveTimeOutOldSdkSkipsValidation) { + auto saved = Libraries::Kernel::g_test_sdk_version; + Libraries::Kernel::g_test_sdk_version = 0x1000000; // pre-1.70 + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // Both small and large usec accepted on old SDK. + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 500u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 5000000u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::g_test_sdk_version = saved; +} + +TEST_F(HttpLifecycle, SetResolveTimeOutNewSdkEnforces) { + auto saved = Libraries::Kernel::g_test_sdk_version; + Libraries::Kernel::g_test_sdk_version = 0x1700000; // exactly 1.70 + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 500000u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 1000000u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 35000000u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::g_test_sdk_version = saved; +} + +// sceHttpSetProxy tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpSetProxy(int, int, int, const char*, u16); +} + +namespace { + +TEST_F(HttpLifecycle, SetProxyHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // mode 1 = manual proxy, host:port specified + EXPECT_EQ(sceHttpSetProxy(tmpl, 1, 0, "proxy.example.com", 8080), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetProxyOnConnection) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + EXPECT_EQ(sceHttpSetProxy(conn, 1, 0, "proxy.example.com", 3128), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetProxyBeforeInit) { + EXPECT_EQ(sceHttpSetProxy(1, 0, 0, "host", 80), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, SetProxyNullHost) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetProxy(tmpl, 1, 0, nullptr, 8080), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetProxyNullHostBeforeBadId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetProxy(99999, 1, 0, nullptr, 8080), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetProxyInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetProxy(99999, 1, 0, "host", 80), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// Connection inherits proxy from template at creation. +TEST_F(HttpLifecycle, SetProxyInheritedByConnection) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + sceHttpSetProxy(tmpl, 1, 0, "tmpl-proxy.example.com", 9999); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + // Override on connection + EXPECT_EQ(sceHttpSetProxy(conn, 1, 0, "conn-proxy.example.com", 8888), ORBIS_OK); + sceHttpTerm(ctx); +} + +} // namespace + +// sceHttpsLoadCert / sceHttpsUnloadCert tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpsLoadCert(int, int, const void**, const void*, const void*); +int PS4_SYSV_ABI sceHttpsUnloadCert(int); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, LoadCertHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + const void* dummy_cas[2] = {nullptr, nullptr}; + EXPECT_EQ(sceHttpsLoadCert(ctx, 2, dummy_cas, nullptr, nullptr), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, LoadCertBeforeInit) { + EXPECT_EQ(sceHttpsLoadCert(1, 0, nullptr, nullptr, nullptr), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, LoadCertInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpsLoadCert(99999, 0, nullptr, nullptr, nullptr), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, UnloadCertHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + const void* dummy_cas[1] = {nullptr}; + sceHttpsLoadCert(ctx, 1, dummy_cas, nullptr, nullptr); + EXPECT_EQ(sceHttpsUnloadCert(ctx), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, UnloadCertWithoutLoadIsOk) { + int ctx = sceHttpInit(0, 0, 4096); + // Idempotent: erasing from the set returns 0 but we don't surface it as + // an error since context exists. + EXPECT_EQ(sceHttpsUnloadCert(ctx), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, UnloadCertBeforeInit) { + EXPECT_EQ(sceHttpsUnloadCert(1), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, UnloadCertInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpsUnloadCert(99999), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, LoadCertSurvivesTermCleanly) { + int ctx = sceHttpInit(0, 0, 4096); + const void* dummy[1] = {nullptr}; + EXPECT_EQ(sceHttpsLoadCert(ctx, 1, dummy, nullptr, nullptr), ORBIS_OK); + sceHttpTerm(ctx); + // After Term, the ctxId is invalid - LoadCert on it must fail. + EXPECT_EQ(sceHttpsLoadCert(ctx, 1, dummy, nullptr, nullptr), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +} // namespace + +// sceHttpsGetCaList / sceHttpsFreeCaList tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpsGetCaList(int httpCtxId, OrbisHttpsCaList* list); +int PS4_SYSV_ABI sceHttpsFreeCaList(int libhttpCtxId, OrbisHttpsCaList* caList); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, GetCaListHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpsCaList list{}; + list.certsNum = 999; // sentinel; should be reset to 0 + EXPECT_EQ(sceHttpsGetCaList(ctx, &list), ORBIS_OK); + EXPECT_EQ(list.certsNum, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, GetCaListBeforeInit) { + OrbisHttpsCaList list{}; + EXPECT_EQ(sceHttpsGetCaList(1, &list), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, GetCaListInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpsCaList list{}; + EXPECT_EQ(sceHttpsGetCaList(99999, &list), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, GetCaListNullList) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpsGetCaList(ctx, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, FreeCaListHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpsCaList list{}; + list.certsNum = 5; // anything non-zero + EXPECT_EQ(sceHttpsFreeCaList(ctx, &list), ORBIS_OK); + EXPECT_EQ(list.certsNum, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, FreeCaListBeforeInit) { + OrbisHttpsCaList list{}; + EXPECT_EQ(sceHttpsFreeCaList(1, &list), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, FreeCaListInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpsCaList list{}; + EXPECT_EQ(sceHttpsFreeCaList(99999, &list), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, FreeCaListNullList) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpsFreeCaList(ctx, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +bool IsFollowableRedirect(int status, s32 method); +s32 MethodAfterRedirect(int status, s32 original_method); +struct ResolvedRedirect { + std::string scheme; + std::string host; + u16 port; + std::string path; +}; +std::optional ResolveRedirectLocation(const std::string& current_scheme, + const std::string& current_host, + u16 current_port, + std::string_view location); +} // namespace Libraries::Http + +namespace { + +using Libraries::Http::IsFollowableRedirect; +using Libraries::Http::MethodAfterRedirect; +using Libraries::Http::ResolveRedirectLocation; + +// --- IsFollowableRedirect: status filter --- + +TEST(Ps4Redirect, FollowsThreeHundred) { + EXPECT_TRUE(IsFollowableRedirect(300, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, FollowsThreeOhOne) { + EXPECT_TRUE(IsFollowableRedirect(301, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, FollowsThreeOhTwo) { + EXPECT_TRUE(IsFollowableRedirect(302, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, FollowsThreeOhThree) { + EXPECT_TRUE(IsFollowableRedirect(303, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, FollowsThreeOhSeven) { + EXPECT_TRUE(IsFollowableRedirect(307, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsThreeOhFour) { + EXPECT_FALSE(IsFollowableRedirect(304, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsThreeOhFive) { + EXPECT_FALSE(IsFollowableRedirect(305, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsThreeOhSix) { + EXPECT_FALSE(IsFollowableRedirect(306, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsThreeOhEight) { + EXPECT_FALSE(IsFollowableRedirect(308, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsNonRedirect) { + EXPECT_FALSE(IsFollowableRedirect(200, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsServerError) { + EXPECT_FALSE(IsFollowableRedirect(500, ORBIS_HTTP_METHOD_GET)); +} + +// --- IsFollowableRedirect: POST nuance (only 303 follows) --- + +TEST(Ps4Redirect, PostFollows303) { + EXPECT_TRUE(IsFollowableRedirect(303, ORBIS_HTTP_METHOD_POST)); +} +TEST(Ps4Redirect, PostDoesNotFollow300) { + EXPECT_FALSE(IsFollowableRedirect(300, ORBIS_HTTP_METHOD_POST)); +} +TEST(Ps4Redirect, PostDoesNotFollow301) { + EXPECT_FALSE(IsFollowableRedirect(301, ORBIS_HTTP_METHOD_POST)); +} +TEST(Ps4Redirect, PostDoesNotFollow302) { + EXPECT_FALSE(IsFollowableRedirect(302, ORBIS_HTTP_METHOD_POST)); +} +TEST(Ps4Redirect, PostDoesNotFollow307) { + EXPECT_FALSE(IsFollowableRedirect(307, ORBIS_HTTP_METHOD_POST)); +} + +// --- IsFollowableRedirect: HEAD always follows where status allows --- + +TEST(Ps4Redirect, HeadFollows301) { + EXPECT_TRUE(IsFollowableRedirect(301, ORBIS_HTTP_METHOD_HEAD)); +} +TEST(Ps4Redirect, HeadFollows303) { + EXPECT_TRUE(IsFollowableRedirect(303, ORBIS_HTTP_METHOD_HEAD)); +} +TEST(Ps4Redirect, HeadFollows307) { + EXPECT_TRUE(IsFollowableRedirect(307, ORBIS_HTTP_METHOD_HEAD)); +} + +// --- MethodAfterRedirect: method change rules --- + +TEST(Ps4Redirect, MethodPreservedOn301) { + EXPECT_EQ(MethodAfterRedirect(301, ORBIS_HTTP_METHOD_GET), ORBIS_HTTP_METHOD_GET); + EXPECT_EQ(MethodAfterRedirect(301, ORBIS_HTTP_METHOD_HEAD), ORBIS_HTTP_METHOD_HEAD); + EXPECT_EQ(MethodAfterRedirect(301, ORBIS_HTTP_METHOD_POST), ORBIS_HTTP_METHOD_POST); +} +TEST(Ps4Redirect, MethodPreservedOn302) { + EXPECT_EQ(MethodAfterRedirect(302, ORBIS_HTTP_METHOD_GET), ORBIS_HTTP_METHOD_GET); + EXPECT_EQ(MethodAfterRedirect(302, ORBIS_HTTP_METHOD_POST), ORBIS_HTTP_METHOD_POST); +} +TEST(Ps4Redirect, MethodPreservedOn307) { + // RFC 7231 §6.4.7 - 307 specifically preserves method + EXPECT_EQ(MethodAfterRedirect(307, ORBIS_HTTP_METHOD_GET), ORBIS_HTTP_METHOD_GET); + EXPECT_EQ(MethodAfterRedirect(307, ORBIS_HTTP_METHOD_POST), ORBIS_HTTP_METHOD_POST); + EXPECT_EQ(MethodAfterRedirect(307, ORBIS_HTTP_METHOD_HEAD), ORBIS_HTTP_METHOD_HEAD); + EXPECT_EQ(MethodAfterRedirect(307, ORBIS_HTTP_METHOD_PUT), ORBIS_HTTP_METHOD_PUT); +} +TEST(Ps4Redirect, PostDowngradesToGetOn303) { + EXPECT_EQ(MethodAfterRedirect(303, ORBIS_HTTP_METHOD_POST), ORBIS_HTTP_METHOD_GET); +} +TEST(Ps4Redirect, PutDowngradesToGetOn303) { + EXPECT_EQ(MethodAfterRedirect(303, ORBIS_HTTP_METHOD_PUT), ORBIS_HTTP_METHOD_GET); +} +TEST(Ps4Redirect, HeadPreservedOn303) { + // RFC 7231 §6.4.4 carve-out: HEAD stays HEAD across 303 + EXPECT_EQ(MethodAfterRedirect(303, ORBIS_HTTP_METHOD_HEAD), ORBIS_HTTP_METHOD_HEAD); +} +TEST(Ps4Redirect, GetStaysGetOn303) { + EXPECT_EQ(MethodAfterRedirect(303, ORBIS_HTTP_METHOD_GET), ORBIS_HTTP_METHOD_GET); +} + +// --- ResolveRedirectLocation: URL parsing --- + +TEST(Ps4Redirect, ResolveAbsoluteHttp) { + auto r = + ResolveRedirectLocation("https", "old.example.com", 443, "http://new.example.com/path?q=1"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "http"); + EXPECT_EQ(r->host, "new.example.com"); + EXPECT_EQ(r->port, 80u); + EXPECT_EQ(r->path, "/path?q=1"); +} +TEST(Ps4Redirect, ResolveAbsoluteHttpsExplicitPort) { + auto r = + ResolveRedirectLocation("http", "old.example.com", 80, "https://secure.example.com:8443/x"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "https"); + EXPECT_EQ(r->host, "secure.example.com"); + EXPECT_EQ(r->port, 8443u); + EXPECT_EQ(r->path, "/x"); +} +TEST(Ps4Redirect, ResolveAbsolutePath) { + auto r = ResolveRedirectLocation("https", "example.com", 443, "/new/path?q=1"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "https"); + EXPECT_EQ(r->host, "example.com"); + EXPECT_EQ(r->port, 443u); + EXPECT_EQ(r->path, "/new/path?q=1"); +} +TEST(Ps4Redirect, ResolveAbsoluteWithoutPath) { + // Bare "https://host" with no path - default to "/" + auto r = ResolveRedirectLocation("http", "old", 80, "https://host.example.com"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "https"); + EXPECT_EQ(r->host, "host.example.com"); + EXPECT_EQ(r->port, 443u); + EXPECT_EQ(r->path, "/"); +} +TEST(Ps4Redirect, ResolveSchemeUppercaseLowercased) { + auto r = ResolveRedirectLocation("http", "x", 80, "HTTPS://host/p"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "https"); // canonicalised +} +TEST(Ps4Redirect, ResolveRejectsDocumentRelative) { + // "foo/bar" - no leading slash, no scheme + auto r = ResolveRedirectLocation("https", "x", 443, "foo/bar"); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsEmpty) { + auto r = ResolveRedirectLocation("https", "x", 443, ""); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsUnknownScheme) { + // ftp:// is not a scheme libhttp speaks; we bail + auto r = ResolveRedirectLocation("https", "x", 443, "ftp://host/path"); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsInvalidPort) { + auto r = ResolveRedirectLocation("https", "x", 443, "https://host:99999/path"); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsZeroPort) { + auto r = ResolveRedirectLocation("https", "x", 443, "https://host:0/path"); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsEmptyAuthority) { + auto r = ResolveRedirectLocation("https", "x", 443, "https:///path"); + EXPECT_FALSE(r.has_value()); +} + +} // namespace + +namespace Libraries::Http { +struct HostOverrideTarget { + std::string scheme; + std::string host; + u16 port = 0; +}; +std::unordered_map ParseHostOverridesJson( + const std::string& json_text); +bool ApplyHostOverride(std::string& scheme, std::string& host, u16& port, bool& is_secure); +} // namespace Libraries::Http + +namespace { + +using Libraries::Http::ApplyHostOverride; +using Libraries::Http::ParseHostOverridesJson; + +// --- ParseHostOverridesJson: shape parsing --- + +TEST(HostOverride, ParseEmptyStringYieldsEmptyMap) { + auto m = ParseHostOverridesJson(""); + EXPECT_TRUE(m.empty()); +} + +TEST(HostOverride, ParseEmptyObjectYieldsEmptyMap) { + auto m = ParseHostOverridesJson("{}"); + EXPECT_TRUE(m.empty()); +} + +TEST(HostOverride, ParseSingleHostNoPortNoScheme) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "localhost"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, ""); // preserve original + EXPECT_EQ(e.host, "localhost"); + EXPECT_EQ(e.port, 0); // preserve original port +} + +TEST(HostOverride, ParseSingleHostWithPortNoScheme) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "localhost:8080"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, ""); + EXPECT_EQ(e.host, "localhost"); + EXPECT_EQ(e.port, 8080); +} + +TEST(HostOverride, ParseHttpSchemeWithPort) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "http://localhost:8080"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, "http"); + EXPECT_EQ(e.host, "localhost"); + EXPECT_EQ(e.port, 8080); +} + +TEST(HostOverride, ParseHttpsSchemeWithPort) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "https://secure.local:8443"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, "https"); + EXPECT_EQ(e.host, "secure.local"); + EXPECT_EQ(e.port, 8443); +} + +TEST(HostOverride, ParseHttpSchemeNoPort) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "http://localhost"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, "http"); + EXPECT_EQ(e.host, "localhost"); + EXPECT_EQ(e.port, 0); +} + +TEST(HostOverride, ParseSchemeUppercaseLowercased) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "HTTPS://Host:443"})"); + ASSERT_EQ(m.size(), 1u); + EXPECT_EQ(m.at("api.example.com").scheme, "https"); +} + +TEST(HostOverride, ParseUnknownSchemeDropsSchemeKeepsHost) { + // ftp:// is not http/https; scheme is silently dropped, host still parsed. + auto m = ParseHostOverridesJson(R"({"api.example.com": "ftp://elsewhere:21"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, ""); + EXPECT_EQ(e.host, "elsewhere"); + EXPECT_EQ(e.port, 21); +} + +TEST(HostOverride, ParseMultipleEntriesMixedForms) { + auto m = ParseHostOverridesJson(R"({ + "api.example.com": "http://localhost:8080", + "analytics.example.com": "https://127.0.0.1:9090", + "static.example.com": "mock.local:7000", + "*": "http://catch-all.local" + })"); + ASSERT_EQ(m.size(), 4u); + EXPECT_EQ(m.at("api.example.com").scheme, "http"); + EXPECT_EQ(m.at("api.example.com").port, 8080); + EXPECT_EQ(m.at("analytics.example.com").scheme, "https"); + EXPECT_EQ(m.at("static.example.com").scheme, ""); // no scheme prefix + EXPECT_EQ(m.at("static.example.com").port, 7000); + EXPECT_EQ(m.at("*").scheme, "http"); + EXPECT_EQ(m.at("*").port, 0); // no port = preserve / default +} + +TEST(HostOverride, ParseCatchAllWildcard) { + auto m = ParseHostOverridesJson(R"({"*": "http://localhost:8080"})"); + ASSERT_EQ(m.size(), 1u); + EXPECT_TRUE(m.contains("*")); + EXPECT_EQ(m.at("*").host, "localhost"); + EXPECT_EQ(m.at("*").scheme, "http"); +} + +TEST(HostOverride, ParseInvalidJsonYieldsEmpty) { + auto m = ParseHostOverridesJson("this is not json"); + EXPECT_TRUE(m.empty()); +} + +TEST(HostOverride, ParseNonObjectRootYieldsEmpty) { + auto m = ParseHostOverridesJson(R"(["array", "not", "object"])"); + EXPECT_TRUE(m.empty()); +} + +TEST(HostOverride, ParseSkipsUnderscorePrefixedKeys) { + auto m = ParseHostOverridesJson(R"({ + "_comment": "this is a comment block", + "_section_apex": "--- Apex Legends endpoints ---", + "api.example.com": "http://localhost:8080", + "_1": "another comment", + "*": "http://catch-all.local" + })"); + ASSERT_EQ(m.size(), 2u); + EXPECT_TRUE(m.contains("api.example.com")); + EXPECT_TRUE(m.contains("*")); + EXPECT_FALSE(m.contains("_comment")); + EXPECT_FALSE(m.contains("_section_apex")); + EXPECT_FALSE(m.contains("_1")); +} + +TEST(HostOverride, ParseSkipsNonStringValues) { + auto m = ParseHostOverridesJson(R"({ + "good.example.com": "http://localhost:8080", + "bad-number.example.com": 1234, + "bad-null.example.com": null, + "bad-object.example.com": {"nested": "no"} + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_TRUE(m.contains("good.example.com")); +} + +TEST(HostOverride, ParseSkipsEmptyStringValues) { + auto m = ParseHostOverridesJson(R"({ + "good.example.com": "localhost", + "empty.example.com": "" + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_TRUE(m.contains("good.example.com")); +} + +TEST(HostOverride, ParseBadPortKeepsHostDropsPort) { + auto m = ParseHostOverridesJson(R"({ + "api.example.com": "http://localhost:not-a-number" + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_EQ(m.at("api.example.com").scheme, "http"); + EXPECT_EQ(m.at("api.example.com").host, "localhost"); + EXPECT_EQ(m.at("api.example.com").port, 0); +} + +TEST(HostOverride, ParseOutOfRangePortKeepsHostDropsPort) { + auto m = ParseHostOverridesJson(R"({ + "api.example.com": "localhost:99999" + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_EQ(m.at("api.example.com").host, "localhost"); + EXPECT_EQ(m.at("api.example.com").port, 0); +} + +TEST(HostOverride, ParseZeroPortKeepsHostDropsPort) { + auto m = ParseHostOverridesJson(R"({ + "api.example.com": "localhost:0" + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_EQ(m.at("api.example.com").host, "localhost"); + EXPECT_EQ(m.at("api.example.com").port, 0); +} + +// --- ApplyHostOverride: behavior with no JSON file present --- +static bool HostOverrideJsonConfigured() { + if (const char* p = std::getenv("SHADPS4_HTTP_HOST_OVERRIDES_JSON"); p && p[0]) { + return true; + } + std::ifstream f("host_overrides.json"); + return f.is_open(); +} + +TEST(HostOverride, InactiveByDefaultLeavesValuesUnchanged) { + if (HostOverrideJsonConfigured()) { + GTEST_SKIP() << "host overrides JSON configured; off-path test inapplicable"; + } + std::string scheme = "https"; + std::string host = "api.example.com"; + u16 port = 443; + bool is_secure = true; + const bool changed = ApplyHostOverride(scheme, host, port, is_secure); + EXPECT_FALSE(changed); + EXPECT_EQ(scheme, "https"); + EXPECT_EQ(host, "api.example.com"); + EXPECT_EQ(port, 443); + EXPECT_TRUE(is_secure); +} + +TEST(HostOverride, InactivePreservesHttpScheme) { + if (HostOverrideJsonConfigured()) { + GTEST_SKIP() << "host overrides JSON configured; off-path test inapplicable"; + } + std::string scheme = "http"; + std::string host = "plain.example.com"; + u16 port = 80; + bool is_secure = false; + const bool changed = ApplyHostOverride(scheme, host, port, is_secure); + EXPECT_FALSE(changed); + EXPECT_EQ(scheme, "http"); + EXPECT_EQ(host, "plain.example.com"); + EXPECT_EQ(port, 80); + EXPECT_FALSE(is_secure); +} + +TEST(HostOverride, InactivePreservesUnusualPort) { + if (HostOverrideJsonConfigured()) { + GTEST_SKIP() << "host overrides JSON configured; off-path test inapplicable"; + } + std::string scheme = "https"; + std::string host = "custom-port.example.com"; + u16 port = 5300; // Pinball FX uses this for kensho-discovery + bool is_secure = true; + ApplyHostOverride(scheme, host, port, is_secure); + EXPECT_EQ(port, 5300); +} + +// --- ApplyHostOverride: behavior WITH the test JSON file pre-staged --- +static const char* kTestOverrideJson = R"({ + "api.example.com": "localhost:8080", + "*": "mock.local:8443" +})"; + +TEST(HostOverride, ActiveExactHostMatchFromJsonFile) { + if (!HostOverrideJsonConfigured()) { + GTEST_SKIP() << "host overrides JSON not configured; on-path test inapplicable"; + } + std::string scheme = "https"; + std::string host = "api.example.com"; + u16 port = 443; + bool is_secure = true; + const bool changed = ApplyHostOverride(scheme, host, port, is_secure); + EXPECT_TRUE(changed); + const bool any_change = (host != "api.example.com") || (port != 443); + EXPECT_TRUE(any_change); +} + +} // namespace