diff --git a/apps/amm/src/SequencerClient.cpp b/apps/amm/src/SequencerClient.cpp index d199895..8de26b7 100644 --- a/apps/amm/src/SequencerClient.cpp +++ b/apps/amm/src/SequencerClient.cpp @@ -56,6 +56,15 @@ QByteArray rpcBody(const QString& method, const QJsonArray& params) }).toJson(QJsonDocument::Compact); } +QUrl canonicalEndpoint(QUrl endpoint) +{ + // Both forms issue requests to the HTTP root, so they must share cache + // and credential identity. + if (endpoint.path() == QLatin1String("/")) + endpoint.setPath({}); + return endpoint; +} + WalletAccountRead accountRead(const QJsonObject& value, const QString& fallbackId) { WalletAccountRead read; @@ -90,8 +99,8 @@ bool SequencerClient::configure(const QString& configPath, if (configValid) { const QJsonDocument document = QJsonDocument::fromJson(file.readAll()); const QJsonObject config = document.object(); - configuredEndpoint = QUrl( - config.value(QStringLiteral("sequencer_addr")).toString()); + configuredEndpoint = canonicalEndpoint(QUrl( + config.value(QStringLiteral("sequencer_addr")).toString())); const QString scheme = configuredEndpoint.scheme().toLower(); configValid = document.isObject() && configuredEndpoint.isValid() @@ -108,8 +117,8 @@ bool SequencerClient::configure(const QString& configPath, } } - QUrl endpoint = effectiveEndpoint.isEmpty() - ? configuredEndpoint : QUrl(effectiveEndpoint); + QUrl endpoint = canonicalEndpoint(effectiveEndpoint.isEmpty() + ? configuredEndpoint : QUrl(effectiveEndpoint)); const QString scheme = endpoint.scheme().toLower(); const bool valid = endpoint.isValid() && !endpoint.host().isEmpty() diff --git a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp index 9cae532..24a1537 100644 --- a/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp +++ b/apps/amm/tests/cpp/NewPositionRuntimeTest.cpp @@ -748,6 +748,79 @@ int main(int argc, char** argv) "matching endpoint should retain configured credentials")) return 1; + LocalRpcServer rootEndpointServer; + if (!expect(rootEndpointServer.listen(), + "root endpoint sequencer should listen")) + return 1; + QTemporaryFile rootEndpointConfig; + if (!expect(rootEndpointConfig.open(), + "root endpoint config should open")) + return 1; + rootEndpointConfig.write(QJsonDocument(QJsonObject { + { QStringLiteral("sequencer_addr"), rootEndpointServer.endpoint() + QLatin1Char('/') }, + { QStringLiteral("basic_auth"), QJsonObject { + { QStringLiteral("username"), QStringLiteral("user") }, + { QStringLiteral("password"), QStringLiteral("pass") }, + } }, + }).toJson(QJsonDocument::Compact)); + rootEndpointConfig.flush(); + FakeAmmClient rootEndpointClient; + SequencerClient rootEndpointSequencer(&rootEndpointClient); + if (!expect(rootEndpointSequencer.configure(rootEndpointConfig.fileName()), + "root endpoint config should configure")) + return 1; + const QString rootEndpointAccount(64, QLatin1Char('4')); + QVector rootEndpointReads; + if (!expect(waitForAccounts(rootEndpointSequencer, { rootEndpointAccount }, false, + &rootEndpointReads), + "root endpoint initial read should complete")) + return 1; + if (!expect(rootEndpointServer.requestCount() == 1 + && rootEndpointServer.lastRequest().toLower().contains( + "authorization: basic dxnlcjpwyxnz"), + "configured root endpoint should use configured credentials")) + return 1; + + if (!expect(rootEndpointSequencer.configure( + rootEndpointConfig.fileName(), rootEndpointServer.endpoint()), + "slash-equivalent endpoint should configure")) + return 1; + rootEndpointReads.clear(); + if (!expect(waitForAccounts(rootEndpointSequencer, { rootEndpointAccount }, false, + &rootEndpointReads), + "slash-equivalent cached read should complete")) + return 1; + if (!expect(rootEndpointServer.requestCount() == 1, + "slash-equivalent endpoint should preserve account cache")) + return 1; + + const QString rootEndpointFreshAccount(64, QLatin1Char('5')); + if (!expect(waitForAccounts(rootEndpointSequencer, { rootEndpointFreshAccount }, true, + &rootEndpointReads), + "slash-equivalent forced read should complete")) + return 1; + if (!expect(rootEndpointServer.requestCount() == 2 + && rootEndpointServer.lastRequest().toLower().contains( + "authorization: basic dxnlcjpwyxnz"), + "slash-equivalent endpoint should retain configured credentials")) + return 1; + + if (!expect(rootEndpointSequencer.configure( + rootEndpointConfig.fileName(), + rootEndpointServer.endpoint() + QStringLiteral("/rpc")), + "path-changing endpoint should configure")) + return 1; + rootEndpointReads.clear(); + if (!expect(waitForAccounts(rootEndpointSequencer, { rootEndpointAccount }, false, + &rootEndpointReads), + "path-changing endpoint read should complete")) + return 1; + if (!expect(rootEndpointServer.requestCount() == 3 + && !rootEndpointServer.lastRequest().toLower().contains( + "authorization:"), + "path-changing endpoint should clear cache and credentials")) + return 1; + LocalRpcServer staleContextServer; if (!expect(staleContextServer.listen(), "stale-context sequencer should listen"))