Skip to content

Commit 2a594b4

Browse files
committed
wsd: test: access_token_ttl tests
Change-Id: I0b213c975fd5265b5ffe1fa12cb5759a9782ff1f Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
1 parent 74dc027 commit 2a594b4

1 file changed

Lines changed: 343 additions & 1 deletion

File tree

test/UnitWOPI.cpp

Lines changed: 343 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
#include <Poco/Net/HTTPRequest.h>
2828

29+
#include <chrono>
2930
#include <thread>
3031
#include <sys/types.h>
3132
#include <unistd.h>
@@ -612,12 +613,353 @@ class UnitWopiHttpHeaders : public WopiTestServer
612613
}
613614
};
614615

616+
/// This tests that loading a document with access_token_ttl
617+
/// works correctly and the token expiry is tracked.
618+
class UnitWopiAccessTokenTtl : public WopiTestServer
619+
{
620+
using Base = WopiTestServer;
621+
622+
STATE_ENUM(Phase, Load, WaitLoadStatus, Modify, Save, Done)
623+
_phase;
624+
625+
public:
626+
UnitWopiAccessTokenTtl()
627+
: WopiTestServer("UnitWopiAccessTokenTtl")
628+
, _phase(Phase::Load)
629+
{
630+
}
631+
632+
/// The document is loaded.
633+
bool onDocumentLoaded(const std::string& message) override
634+
{
635+
TST_LOG("Got: [" << message << ']');
636+
LOK_ASSERT_STATE(_phase, Phase::WaitLoadStatus);
637+
638+
TRANSITION_STATE(_phase, Phase::Modify);
639+
640+
// Modify the currently opened document; type 'a'.
641+
WSD_CMD("key type=input char=97 key=0");
642+
WSD_CMD("key type=up char=0 key=512");
643+
644+
return true;
645+
}
646+
647+
bool onDocumentModified(const std::string& message) override
648+
{
649+
TST_LOG("Got: [" << message << ']');
650+
LOK_ASSERT_STATE(_phase, Phase::Modify);
651+
652+
TRANSITION_STATE(_phase, Phase::Save);
653+
WSD_CMD("save dontTerminateEdit=0 dontSaveIfUnmodified=0");
654+
655+
return true;
656+
}
657+
658+
std::unique_ptr<http::Response>
659+
assertPutFileRequest(const Poco::Net::HTTPRequest& /*request*/) override
660+
{
661+
LOK_ASSERT_STATE(_phase, Phase::Save);
662+
TRANSITION_STATE(_phase, Phase::Done);
663+
664+
passTest("PutFile succeeded with access_token_ttl");
665+
return nullptr;
666+
}
667+
668+
void invokeWSDTest() override
669+
{
670+
switch (_phase)
671+
{
672+
case Phase::Load:
673+
{
674+
TRANSITION_STATE(_phase, Phase::WaitLoadStatus);
675+
676+
// TTL 15 minutes from now, in milliseconds (as per WOPI spec).
677+
const uint64_t expiryTimeMs =
678+
static_cast<uint64_t>(time(nullptr) + (15 * 60)) * 1000;
679+
initWebsocket("/wopi/files/0?access_token=secret&access_token_ttl=" +
680+
std::to_string(expiryTimeMs));
681+
682+
WSD_CMD("load url=" + getWopiSrc());
683+
break;
684+
}
685+
case Phase::WaitLoadStatus:
686+
case Phase::Modify:
687+
case Phase::Save:
688+
case Phase::Done:
689+
{
690+
// just wait for the results
691+
break;
692+
}
693+
}
694+
}
695+
};
696+
697+
/// Test the save -> 401 -> tokenexpired -> resetaccesstoken -> retry -> success flow.
698+
class UnitWopiTokenRefreshOnSave : public WopiTestServer
699+
{
700+
using Base = WopiTestServer;
701+
702+
STATE_ENUM(Phase, Load, WaitLoadStatus, Modify, WaitSave, WaitTokenExpired, Done)
703+
_phase;
704+
705+
int _putFileCount;
706+
707+
static constexpr auto OriginalToken = "original_token_123";
708+
static constexpr auto RefreshedToken = "refreshed_token_456";
709+
710+
public:
711+
UnitWopiTokenRefreshOnSave()
712+
: WopiTestServer("UnitWopiTokenRefreshOnSave")
713+
, _phase(Phase::Load)
714+
, _putFileCount(0)
715+
{
716+
}
717+
718+
bool onDocumentLoaded(const std::string& message) override
719+
{
720+
TST_LOG("Got: [" << message << ']');
721+
LOK_ASSERT_STATE(_phase, Phase::WaitLoadStatus);
722+
723+
TRANSITION_STATE(_phase, Phase::Modify);
724+
725+
// Modify the document; type 'a'.
726+
WSD_CMD("key type=input char=97 key=0");
727+
WSD_CMD("key type=up char=0 key=512");
728+
729+
return true;
730+
}
731+
732+
bool onDocumentModified(const std::string& message) override
733+
{
734+
TST_LOG("Got: [" << message << ']');
735+
LOK_ASSERT_STATE(_phase, Phase::Modify);
736+
737+
TRANSITION_STATE(_phase, Phase::WaitSave);
738+
WSD_CMD("save dontTerminateEdit=0 dontSaveIfUnmodified=0");
739+
740+
return true;
741+
}
742+
743+
std::unique_ptr<http::Response>
744+
assertPutFileRequest(const Poco::Net::HTTPRequest& request) override
745+
{
746+
++_putFileCount;
747+
TST_LOG("PutFile #" << _putFileCount << " URI: " << request.getURI());
748+
749+
if (_putFileCount == 1)
750+
{
751+
// First PutFile: verify original token and return 401.
752+
for (const auto& param : Poco::URI(request.getURI()).getQueryParameters())
753+
{
754+
if (param.first == "access_token")
755+
{
756+
LOK_ASSERT_EQUAL_STR(std::string(OriginalToken), param.second);
757+
break;
758+
}
759+
}
760+
761+
TST_LOG("Returning 401 to trigger token refresh");
762+
return std::make_unique<http::Response>(http::StatusCode::Unauthorized);
763+
}
764+
765+
// Second PutFile: verify the refreshed token is used.
766+
LOK_ASSERT_EQUAL(2, _putFileCount);
767+
for (const auto& param : Poco::URI(request.getURI()).getQueryParameters())
768+
{
769+
if (param.first == "access_token")
770+
{
771+
LOK_ASSERT_EQUAL_STR(std::string(RefreshedToken), param.second);
772+
break;
773+
}
774+
}
775+
776+
TRANSITION_STATE(_phase, Phase::Done);
777+
passTest("PutFile retry with refreshed token succeeded");
778+
return nullptr;
779+
}
780+
781+
bool onFilterSendWebSocketMessage(const std::string_view message, const WSOpCode /* code */,
782+
const bool /* flush */, int& /*unitReturn*/) override
783+
{
784+
if (message == "tokenexpired")
785+
{
786+
TST_LOG("Got tokenexpired, sending resetaccesstoken with new token and TTL");
787+
LOK_ASSERT_STATE(_phase, Phase::WaitSave);
788+
TRANSITION_STATE(_phase, Phase::WaitTokenExpired);
789+
790+
// TTL 30 minutes from now, in milliseconds.
791+
const uint64_t expiryMs =
792+
static_cast<uint64_t>(time(nullptr) + 30 * 60) * 1000;
793+
WSD_CMD("resetaccesstoken " + std::string(RefreshedToken) + ' ' +
794+
std::to_string(expiryMs));
795+
}
796+
797+
return false;
798+
}
799+
800+
void invokeWSDTest() override
801+
{
802+
switch (_phase)
803+
{
804+
case Phase::Load:
805+
{
806+
TRANSITION_STATE(_phase, Phase::WaitLoadStatus);
807+
808+
initWebsocket("/wopi/files/0?access_token=" + std::string(OriginalToken) +
809+
"&access_token_ttl=0");
810+
811+
WSD_CMD("load url=" + getWopiSrc());
812+
break;
813+
}
814+
case Phase::WaitLoadStatus:
815+
case Phase::Modify:
816+
case Phase::WaitSave:
817+
case Phase::WaitTokenExpired:
818+
case Phase::Done:
819+
{
820+
// just wait for the results
821+
break;
822+
}
823+
}
824+
}
825+
};
826+
827+
/// Test the save -> 401 -> tokenexpired -> no refresh -> saveunauthorized flow.
828+
/// When no resetaccesstoken is sent, the second upload retry also gets 401,
829+
/// which triggers immediate failure (since _pendingTokenRefresh is already active).
830+
class UnitWopiTokenRefreshTimeout : public WopiTestServer
831+
{
832+
using Base = WopiTestServer;
833+
834+
STATE_ENUM(Phase, Load, WaitLoadStatus, Modify, WaitSave, WaitTimeout, Done)
835+
_phase;
836+
int _defLifetimeMins;
837+
int _refreshTimeoutSecs;
838+
int _ttlOffsetMins;
839+
bool _gotTokenExpired;
840+
841+
public:
842+
UnitWopiTokenRefreshTimeout(int defLifetimeMins, int refreshTimeoutSecs, int ttlOffsetMins)
843+
: WopiTestServer("UnitWopiTokenRefreshTimeout")
844+
, _phase(Phase::Load)
845+
, _defLifetimeMins(defLifetimeMins)
846+
, _refreshTimeoutSecs(refreshTimeoutSecs)
847+
, _ttlOffsetMins(ttlOffsetMins)
848+
, _gotTokenExpired(false)
849+
{
850+
}
851+
852+
void configure(Poco::Util::LayeredConfiguration& config) override
853+
{
854+
WopiTestServer::configure(config);
855+
856+
config.setUInt("storage.wopi.access_token.default_lifetime_mins", _defLifetimeMins);
857+
config.setUInt("storage.wopi.access_token.refresh_timeout_secs", _refreshTimeoutSecs);
858+
config.setUInt("storage.wopi.access_token.ttl_offset_minutes", _ttlOffsetMins);
859+
}
860+
861+
bool onDocumentLoaded(const std::string& message) override
862+
{
863+
TST_LOG("Got: [" << message << ']');
864+
LOK_ASSERT_STATE(_phase, Phase::WaitLoadStatus);
865+
866+
TRANSITION_STATE(_phase, Phase::Modify);
867+
868+
// Modify the document; type 'a'.
869+
WSD_CMD("key type=input char=97 key=0");
870+
WSD_CMD("key type=up char=0 key=512");
871+
872+
return true;
873+
}
874+
875+
bool onDocumentModified(const std::string& message) override
876+
{
877+
TST_LOG("Got: [" << message << ']');
878+
LOK_ASSERT_STATE(_phase, Phase::Modify);
879+
880+
TRANSITION_STATE(_phase, Phase::WaitSave);
881+
WSD_CMD("save dontTerminateEdit=0 dontSaveIfUnmodified=0");
882+
883+
return true;
884+
}
885+
886+
std::unique_ptr<http::Response>
887+
assertPutFileRequest(const Poco::Net::HTTPRequest& /*request*/) override
888+
{
889+
// Always return 401. The first 401 triggers tokenexpired and sets
890+
// _pendingTokenRefresh.active. The upload retry fires a second PutFile;
891+
// that second 401 hits the else branch (already active) and immediately
892+
// sends saveunauthorized.
893+
TST_LOG("Returning 401 (no token refresh will arrive)");
894+
return std::make_unique<http::Response>(http::StatusCode::Unauthorized);
895+
}
896+
897+
bool onFilterSendWebSocketMessage(const std::string_view message, const WSOpCode /* code */,
898+
const bool /* flush */, int& /*unitReturn*/) override
899+
{
900+
if (message == "tokenexpired")
901+
{
902+
TST_LOG("Got tokenexpired, deliberately NOT sending resetaccesstoken");
903+
LOK_ASSERT_STATE(_phase, Phase::WaitSave);
904+
TRANSITION_STATE(_phase, Phase::WaitTimeout);
905+
_gotTokenExpired = true;
906+
// Let the refresh-request time out.
907+
}
908+
909+
return false;
910+
}
911+
912+
bool onDocumentError(const std::string& message) override
913+
{
914+
TST_LOG("Got error: [" << message << ']');
915+
916+
if (message.find("kind=saveunauthorized") != std::string::npos)
917+
{
918+
LOK_ASSERT_STATE(_phase, Phase::WaitTimeout);
919+
LOK_ASSERT_MESSAGE("tokenexpired should have been sent before timeout", _gotTokenExpired);
920+
921+
TRANSITION_STATE(_phase, Phase::Done);
922+
passTest("Token refresh timeout correctly produced saveunauthorized");
923+
return true;
924+
}
925+
926+
return false;
927+
}
928+
929+
void invokeWSDTest() override
930+
{
931+
switch (_phase)
932+
{
933+
case Phase::Load:
934+
{
935+
TRANSITION_STATE(_phase, Phase::WaitLoadStatus);
936+
937+
initWebsocket("/wopi/files/0?access_token=anything&access_token_ttl=0");
938+
939+
WSD_CMD("load url=" + getWopiSrc());
940+
break;
941+
}
942+
case Phase::WaitLoadStatus:
943+
case Phase::Modify:
944+
case Phase::WaitSave:
945+
case Phase::WaitTimeout:
946+
case Phase::Done:
947+
{
948+
// just wait for the results
949+
break;
950+
}
951+
}
952+
}
953+
};
954+
615955
UnitBase** unit_create_wsd_multi(void)
616956
{
617957
return new UnitBase* []
618958
{
619959
new UnitWOPI(), new UnitWOPILoadEncoded(),
620-
/*new UnitOverload(),*/ new UnitWopiUnavailable(), new UnitWopiHttpHeaders(), nullptr
960+
/*new UnitOverload(),*/ new UnitWopiUnavailable(), new UnitWopiHttpHeaders(),
961+
new UnitWopiAccessTokenTtl(), new UnitWopiTokenRefreshOnSave(),
962+
new UnitWopiTokenRefreshTimeout(0, 5, 0), nullptr
621963
};
622964
}
623965

0 commit comments

Comments
 (0)