Repository: magnopus-opensource/connected-spaces-platform
Pull request #634: [OF-1572] chore: remove credentials file support from MultiPlayerTestRunner
Author: login: MAG-OR
Created: 2025-03-07T08:57:52Z | Merged: 2025-03-24T10:54:42Z

- Make email and password compulsory arguments
- Remove support for the credentials file backup mechanism
- Create temporary test accounts before logging in



Diff:
diff --git a/MultiplayerTestRunner/include/ErrorCodes.h b/MultiplayerTestRunner/include/ErrorCodes.h
index 25bf0c2b9..b981afc84 100644
--- a/MultiplayerTestRunner/include/ErrorCodes.h
+++ b/MultiplayerTestRunner/include/ErrorCodes.h
@@ -23,8 +23,8 @@
 namespace MultiplayerTestRunner::ErrorCodes
 {
 constexpr const int SUCCESS = 0;
-constexpr const int COULD_NOT_FIND_CREDENTIALS_FILE = 100;
-constexpr const int MALFORMED_CREDENTIALS_FILE = 101;
+constexpr const int FAILED_TO_CREATE_USER = 100;
+constexpr const int INVALID_LOGIN_PASSWORD = 101;
 constexpr const int FAILED_TO_LOGIN = 102;
 constexpr const int INVALID_TEST_SPECIFIER = 103;
 constexpr const int OTHER_EXCEPTION = 104; // For any exception that isn't thrown explicitly by the Multiplayer Test Runner
diff --git a/MultiplayerTestRunner/src/CLIArgs.cpp b/MultiplayerTestRunner/src/CLIArgs.cpp
index c09751e04..2eb20dacc 100644
--- a/MultiplayerTestRunner/src/CLIArgs.cpp
+++ b/MultiplayerTestRunner/src/CLIArgs.cpp
@@ -28,32 +28,16 @@ constexpr int DEFAULT_TIMEOUT_IN_SECONDS = 30;
 
 /*
  * Take the raw input from the CLI, validate, and convert to structured data.
- * If Login or Password not provided, this will attempt to read `test_account_creds.txt`, and throw if it cannot.
- * TestIdentifier is the only mandatory arg, this will validate that the string provided is an actual identifier.
+ * If Login and Password are not provided, this will throw an error.
+ * TestIdentifier is also a mandatory arg, this will validate that the string provided is an actual identifier.
  * For optional arguments, if empty data is provided, will populate them with defaults.
  * SpaceId will not be defaulted, and will be left empty if empty, in the expectation a space is about to be created and the value set externally.
  */
-CLIArgs::RunnerSettings ValidateInvocationArgs(const std::string& TestIdentifier, std::optional<std::string> LoginEmail,
-    std::optional<std::string> LoginPassword, const std::optional<std::string>& Endpoint, const std::optional<std::string>& SpaceId,
-    const std::optional<int>& TimeoutInSeconds)
+CLIArgs::RunnerSettings ValidateInvocationArgs(const std::string& TestIdentifier, std::string LoginEmail, std::string LoginPassword,
+    const std::optional<std::string>& Endpoint, const std::optional<std::string>& SpaceId, const std::optional<int>& TimeoutInSeconds)
 {
-    if (!LoginEmail.has_value() && !LoginPassword.has_value())
-    {
-        std::cout << "Credentials not provided, attempting to find credentials file.\n";
-        Utils::TestAccountCredentials credentials = Utils::LoadTestAccountCredentials();
-        LoginEmail = credentials.DefaultLoginEmail;
-        LoginPassword = credentials.DefaultLoginPassword;
-    }
-    else if (!LoginEmail.has_value() || !LoginPassword.has_value())
-    {
-        // If only one of the email/password pair has been provided, error out entirely, it's probably a mistake
-        throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR,
-            "Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-            "wish to use the credentials file.");
-    }
-
     CLIArgs::RunnerSettings settings;
-    // Test identifiers need to be valid, and are not optional. An incorrect one is grounds to abort.
+    // Test identifier, emails and passwords need to be valid, and are not optional. An incorrect one is grounds to abort.
     try
     {
         settings.TestIdentifier = MultiplayerTestRunner::TestIdentifiers::StringToTestIdentifier(TestIdentifier);
@@ -63,7 +47,15 @@ CLIArgs::RunnerSettings ValidateInvocationArgs(const std::string& TestIdentifier
         // The reason this rethrow is here is an annoying quirk of `StringToTestIdentifier` being a public method.
         throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::INVALID_TEST_SPECIFIER, exception.what());
     }
-    settings.LoginEmailAndPassword = std::make_pair(LoginEmail.value(), LoginPassword.value());
+    try
+    {
+        settings.LoginEmailAndPassword = std::make_pair(LoginEmail, LoginPassword);
+    }
+    catch (std::exception& exception)
+    {
+        // If the email/password pair has not been provided, error out
+        throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::INVALID_LOGIN_PASSWORD, exception.what());
+    }
     settings.Endpoint = Endpoint.value_or(DEFAULT_TEST_ENDPOINT);
     settings.TimeoutInSeconds = TimeoutInSeconds.value_or(DEFAULT_TIMEOUT_IN_SECONDS);
     settings.SpaceId = SpaceId; // This value is not defaulted, as an empty value means a space is about to be created, and this value will be set
@@ -85,17 +77,17 @@ RunnerSettings ProcessCLI(int argc, char* argv[])
     App.description(AppDescription);
 
     // These raw CLI variables populated in CLI11_PARSE, and then converted into validated and structured data below in `ValidateInvocationArgs`.
-    std::string TestIdentifier;
+    std::string TestIdentifier, LoginEmail, LoginPassword;
     std::optional<int> TimeoutInSeconds;
-    std::optional<std::string> Endpoint, LoginEmail, LoginPassword, SpaceId;
+    std::optional<std::string> Endpoint, SpaceId;
 
     App.add_option("-t,--test", TestIdentifier, "The test to run. See `include/TestIdentifiers.h` for available options.")->required();
-    App.add_option("-e,--email", LoginEmail,
-        "Login email for the test CHS account. If not set, the application will attempt to source this from a `test_account_creds.txt` "
-        "next to the binary.");
-    App.add_option("-p,--password", LoginPassword,
-        "Password for the test CHS account. If not set, the application will attempt to source this from a `test_account_creds.txt` next "
-        "to the binary.");
+    App.add_option(
+           "-e,--email", LoginEmail, "Login email for the test CHS account. The account must already exist. If it does not exist, errors out.`")
+        ->required();
+    App.add_option(
+           "-p,--password", LoginPassword, "Password for the test CHS account. The account must already exist. If it does not exist, errors out.")
+        ->required();
     App.add_option("-s,--space", SpaceId,
         "SpaceId to use in the invoked test. If none is provided, creates a random space. If a space id is provided, the space is assumed "
         "to already exist, and will not be cleaned up.");
diff --git a/MultiplayerTestRunner/src/InternalUnitTests/Test_CLI.cpp b/MultiplayerTestRunner/src/InternalUnitTests/Test_CLI.cpp
index c3d707e1b..157223cc2 100644
--- a/MultiplayerTestRunner/src/InternalUnitTests/Test_CLI.cpp
+++ b/MultiplayerTestRunner/src/InternalUnitTests/Test_CLI.cpp
@@ -56,10 +56,12 @@ TEST(CLITest, TestIdentifierRequired)
     }
 }
 
-TEST(CLITest, WhenInvalidTestIdentiferThenExceptionThrow)
+TEST(CLITest, LoginEmailRequired)
 {
-    std::vector<char*> args
-        = { "MultiplayerTestRunner", "--test", "NotARealTestIdentifier", "--email", "test@example.com", "--password", "password123" };
+    using namespace MultiplayerTestRunner::TestIdentifiers;
+    std::string TestID = TestIdentifierToString(TestIdentifier::CREATE_AVATAR);
+    std::vector<char*> args = { "MultiplayerTestRunner", "--test", TestID.data(), "--password", "password123", "--space", "space-id-123", "--timeout",
+        "60", "--endpoint", "https://example.com" };
 
     try
     {
@@ -67,8 +69,8 @@ TEST(CLITest, WhenInvalidTestIdentiferThenExceptionThrow)
     }
     catch (const Utils::ExceptionWithCode& Exception)
     {
-        EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::INVALID_TEST_SPECIFIER);
-        EXPECT_EQ(std::string(Exception.what()), std::string("String `NotARealTestIdentifier` does not match any TestIdentifier"));
+        EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR);
+        EXPECT_EQ(std::string(Exception.what()), std::string("--email is required"));
     }
     catch (...)
     {
@@ -76,28 +78,12 @@ TEST(CLITest, WhenInvalidTestIdentiferThenExceptionThrow)
     }
 }
 
-TEST(CLITest, DefaultsSet)
-{
-    using namespace MultiplayerTestRunner::TestIdentifiers;
-    std::string TestID = TestIdentifierToString(TestIdentifier::CREATE_AVATAR);
-    std::vector<char*> args = { "MultiplayerTestRunner", "--test", TestID.data(), "--email", "test@example.com", "--password", "password123" };
-    CLIArgs::RunnerSettings Settings = CLIArgs::ProcessCLI(static_cast<int>(args.size()), args.data());
-
-    // We don't want to go to the credentials file for the defaults, so we provide a user/password
-    EXPECT_EQ(Settings.LoginEmailAndPassword.first, "test@example.com");
-    EXPECT_EQ(Settings.LoginEmailAndPassword.second, "password123");
-    EXPECT_EQ(Settings.TestIdentifier, TestIdentifier::CREATE_AVATAR);
-    EXPECT_EQ(Settings.Endpoint, "https://ogs-internal.magnopus-dev.cloud");
-    EXPECT_EQ(Settings.TimeoutInSeconds, 30);
-    EXPECT_FALSE(Settings.SpaceId.has_value());
-}
-
-/* This test tests different paths depending on if there's a credentials file or not */
-TEST(CLITest, WhenNoEmailThenError)
+TEST(CLITest, PasswordRequired)
 {
     using namespace MultiplayerTestRunner::TestIdentifiers;
     std::string TestID = TestIdentifierToString(TestIdentifier::CREATE_AVATAR);
-    std::vector<char*> args = { "MultiplayerTestRunner", "--test", TestID.data(), "--password", "password123" };
+    std::vector<char*> args = { "MultiplayerTestRunner", "--test", TestID.data(), "--email", "test@example.com", "--password", "ergeqrheh", "--space",
+        "space-id-123", "--timeout", "60", "--endpoint", "https://example.com" };
 
     try
     {
@@ -105,11 +91,8 @@ TEST(CLITest, WhenNoEmailThenError)
     }
     catch (const Utils::ExceptionWithCode& Exception)
     {
-        // If there's no credentials file, we'll throw an error.
         EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR);
-        EXPECT_EQ(std::string(Exception.what()),
-            std::string("Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-                        "wish to use the credentials file."));
+        EXPECT_EQ(std::string(Exception.what()), std::string("--password is required"));
     }
     catch (...)
     {
@@ -117,12 +100,10 @@ TEST(CLITest, WhenNoEmailThenError)
     }
 }
 
-/* This test tests different paths depending on if there's a credentials file or not */
-TEST(CLITest, WhenNoPasswordThenError)
+TEST(CLITest, WhenInvalidTestIdentiferThenExceptionThrow)
 {
-    using namespace MultiplayerTestRunner::TestIdentifiers;
-    std::string TestID = TestIdentifierToString(TestIdentifier::CREATE_AVATAR);
-    std::vector<char*> args = { "MultiplayerTestRunner", "--test", TestID.data(), "--email", "test@example.com" };
+    std::vector<char*> args
+        = { "MultiplayerTestRunner", "--test", "NotARealTestIdentifier", "--email", "test@example.com", "--password", "password123" };
 
     try
     {
@@ -130,10 +111,8 @@ TEST(CLITest, WhenNoPasswordThenError)
     }
     catch (const Utils::ExceptionWithCode& Exception)
     {
-        EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR);
-        EXPECT_EQ(std::string(Exception.what()),
-            std::string("Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-                        "wish to use the credentials file."));
+        EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::INVALID_TEST_SPECIFIER);
+        EXPECT_EQ(std::string(Exception.what()), std::string("String `NotARealTestIdentifier` does not match any TestIdentifier"));
     }
     catch (...)
     {
@@ -141,31 +120,17 @@ TEST(CLITest, WhenNoPasswordThenError)
     }
 }
 
-/* This test tests different paths depending on if there's a credentials file or not */
-TEST(CLITest, WhenNoCredentialsOnCLIThenCredentialsFileIsQueried)
+TEST(CLITest, DefaultsSet)
 {
     using namespace MultiplayerTestRunner::TestIdentifiers;
     std::string TestID = TestIdentifierToString(TestIdentifier::CREATE_AVATAR);
-    std::vector<char*> args = { "MultiplayerTestRunner", "--test", TestID.data() };
+    std::vector<char*> args = { "MultiplayerTestRunner", "--test", TestID.data(), "--email", "test@example.com", "--password", "password123" };
+    CLIArgs::RunnerSettings Settings = CLIArgs::ProcessCLI(static_cast<int>(args.size()), args.data());
 
-    try
-    {
-        CLIArgs::RunnerSettings Settings = CLIArgs::ProcessCLI(static_cast<int>(args.size()), args.data());
-        // If there is a credentials file, we'll now have a username and a password
-        EXPECT_TRUE(Settings.LoginEmailAndPassword.first.length() > 0);
-        EXPECT_TRUE(Settings.LoginEmailAndPassword.second.length() > 0);
-    }
-    catch (const Utils::ExceptionWithCode& Exception)
-    {
-        // If there's no credentials file, we'll throw an error.
-        EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::COULD_NOT_FIND_CREDENTIALS_FILE);
-        EXPECT_EQ(std::string(Exception.what()),
-            std::string(
-                "test_account_creds.txt not found! This file must exist and must contain the following information:\n<DefaultLoginEmail> "
-                "<DefaultLoginPassword>\n<AlternativeLoginEmail> <AlternativeLoginPassword>\n<SuperUserLoginEmail> <SuperUserLoginPassword>"));
-    }
-    catch (...)
-    {
-        FAIL() << "Unexpected exception type thrown";
-    }
-}
\ No newline at end of file
+    EXPECT_EQ(Settings.LoginEmailAndPassword.first, "test@example.com");
+    EXPECT_EQ(Settings.LoginEmailAndPassword.second, "password123");
+    EXPECT_EQ(Settings.TestIdentifier, TestIdentifier::CREATE_AVATAR);
+    EXPECT_EQ(Settings.Endpoint, "https://ogs-internal.magnopus-dev.cloud");
+    EXPECT_EQ(Settings.TimeoutInSeconds, 30);
+    EXPECT_FALSE(Settings.SpaceId.has_value());
+}
diff --git a/MultiplayerTestRunner/src/InternalUnitTests/Test_LoginRAII.cpp b/MultiplayerTestRunner/src/InternalUnitTests/Test_LoginRAII.cpp
index df660021d..f6813682f 100644
--- a/MultiplayerTestRunner/src/InternalUnitTests/Test_LoginRAII.cpp
+++ b/MultiplayerTestRunner/src/InternalUnitTests/Test_LoginRAII.cpp
@@ -23,23 +23,7 @@
 #include <gtest/gtest.h>
 #include <optional>
 
-namespace
-{
-/* Some tests only run if there's a credentials file */
-std::optional<Utils::TestAccountCredentials> CredentialsFromFile()
-{
-    try
-    {
-        return Utils::LoadTestAccountCredentials();
-    }
-    catch (...)
-    {
-        return {};
-    }
-}
-} // namespace
-
-/* Initialze CSP before the suite begins with a fixture */
+/* Initialize CSP before the suite begins with a fixture */
 class LoginRAIITest : public ::testing::Test
 {
 protected:
@@ -48,16 +32,11 @@ class LoginRAIITest : public ::testing::Test
 
 TEST_F(LoginRAIITest, TestValidLogin)
 {
-    std::optional<Utils::TestAccountCredentials> Credentials = CredentialsFromFile();
-    if (!Credentials.has_value())
-    {
-        GTEST_SKIP() << "No credentials file found, Skipping Test.";
-    }
-
     {
         // Check the process descriptor is printed.
         ::testing::internal::CaptureStdout();
-        LoginRAII login { Credentials.value().DefaultLoginEmail, Credentials.value().DefaultLoginPassword };
+        auto TestUser = Utils::CreateTestUser();
+        LoginRAII login { TestUser.Email.c_str(), Utils::GeneratedTestAccountPassword };
         EXPECT_NE(::testing::internal::GetCapturedStdout().find(MultiplayerTestRunner::ProcessDescriptors::LOGGED_IN_DESCRIPTOR), std::string::npos);
         ::testing::internal::CaptureStdout(); // GetCapturedStdout stops capturing, so we want to start again for the logout descriptor
     }
diff --git a/MultiplayerTestRunner/src/InternalUnitTests/Test_RunnableTests.cpp b/MultiplayerTestRunner/src/InternalUnitTests/Test_RunnableTests.cpp
index 58c770218..9ce718b2c 100644
--- a/MultiplayerTestRunner/src/InternalUnitTests/Test_RunnableTests.cpp
+++ b/MultiplayerTestRunner/src/InternalUnitTests/Test_RunnableTests.cpp
@@ -30,23 +30,7 @@
  * Nonetheless, we should ensure they don't crash.
  */
 
-namespace
-{
-/* Some tests only run if there's a credentials file */
-std::optional<Utils::TestAccountCredentials> CredentialsFromFile()
-{
-    try
-    {
-        return Utils::LoadTestAccountCredentials();
-    }
-    catch (...)
-    {
-        return {};
-    }
-}
-} // namespace
-
-/* Initialze CSP before the suite begins with a fixture */
+/* Initialize CSP before the suite begins with a fixture */
 class RunnableTests : public ::testing::Test
 {
 protected:
@@ -55,14 +39,9 @@ class RunnableTests : public ::testing::Test
 
 TEST_F(RunnableTests, CreateAvatar)
 {
-    std::optional<Utils::TestAccountCredentials> Credentials = CredentialsFromFile();
-    if (!Credentials.has_value())
-    {
-        GTEST_SKIP() << "No credentials file found, Skipping Test.";
-    }
-
     // Login
-    LoginRAII login { Credentials.value().DefaultLoginEmail, Credentials.value().DefaultLoginPassword };
+    auto TestUser = Utils::CreateTestUser();
+    LoginRAII login { TestUser.Email.c_str(), Utils::GeneratedTestAccountPassword };
     // Make a throwaway space
     SpaceRAII Space({});
 
diff --git a/MultiplayerTestRunner/src/InternalUnitTests/Test_SpaceRAII.cpp b/MultiplayerTestRunner/src/InternalUnitTests/Test_SpaceRAII.cpp
index bc3187ea9..283630b8c 100644
--- a/MultiplayerTestRunner/src/InternalUnitTests/Test_SpaceRAII.cpp
+++ b/MultiplayerTestRunner/src/InternalUnitTests/Test_SpaceRAII.cpp
@@ -26,23 +26,7 @@
 #include <gtest/gtest.h>
 #include <optional>
 
-namespace
-{
-/* Some tests only run if there's a credentials file */
-std::optional<Utils::TestAccountCredentials> CredentialsFromFile()
-{
-    try
-    {
-        return Utils::LoadTestAccountCredentials();
-    }
-    catch (...)
-    {
-        return {};
-    }
-}
-} // namespace
-
-/* Initialze CSP before the suite begins with a fixture */
+/* Initialize CSP before the suite begins with a fixture */
 class SpaceRAIITest : public ::testing::Test
 {
 protected:
@@ -51,16 +35,11 @@ class SpaceRAIITest : public ::testing::Test
 
 TEST_F(SpaceRAIITest, TestCreateNewSpaceWhenLoggedIn)
 {
-    std::optional<Utils::TestAccountCredentials> Credentials = CredentialsFromFile();
-    if (!Credentials.has_value())
-    {
-        GTEST_SKIP() << "No credentials file found, Skipping Test.";
-    }
     ::testing::internal::CaptureStdout();
 
     // Login
-    LoginRAII login { Credentials.value().DefaultLoginEmail, Credentials.value().DefaultLoginPassword };
-
+    auto TestUser = Utils::CreateTestUser();
+    LoginRAII login { TestUser.Email.c_str(), Utils::GeneratedTestAccountPassword };
     {
         // Check the create space and joined space process descriptors are printed when we don't provide a spaceID
         SpaceRAII Space({});
@@ -108,15 +87,9 @@ TEST_F(SpaceRAIITest, TestCreateNewSpaceWhenNotLoggedIn)
 
 TEST_F(SpaceRAIITest, TestUseExistingSpace)
 {
-    std::optional<Utils::TestAccountCredentials> Credentials = CredentialsFromFile();
-    if (!Credentials.has_value())
-    {
-        GTEST_SKIP() << "No credentials file found, Skipping Test.";
-    }
-
     // Login
-    LoginRAII login { Credentials.value().DefaultLoginEmail, Credentials.value().DefaultLoginPassword };
-
+    auto TestUser = Utils::CreateTestUser();
+    LoginRAII login { TestUser.Email.c_str(), Utils::GeneratedTestAccountPassword };
     // Create a space
     auto& SystemsManager = csp::systems::SystemsManager::Get();
     auto& SpaceSystem = *SystemsManager.GetSpaceSystem();
diff --git a/MultiplayerTestRunner/src/LoginRAII.cpp b/MultiplayerTestRunner/src/LoginRAII.cpp
index 25d8c2ccf..58d744d06 100644
--- a/MultiplayerTestRunner/src/LoginRAII.cpp
+++ b/MultiplayerTestRunner/src/LoginRAII.cpp
@@ -53,7 +53,7 @@ csp::common::String LogIn(csp::systems::UserSystem& UserSystem, const csp::commo
     else
     {
         const std::string msg = "Failed to login to service, got result code " + std::to_string(static_cast<uint8_t>(LoginResult.GetResultCode()))
-            + "\n Response Body: " + LoginResult.GetResponseBody().c_str();
+            + ". Have you created the user account?" + "\n Response Body: " + LoginResult.GetResponseBody().c_str();
         throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::FAILED_TO_LOGIN, msg);
     }
 }
diff --git a/MultiplayerTestRunner/src/Utils.cpp b/MultiplayerTestRunner/src/Utils.cpp
index 053dc3eba..2a3fee2a6 100644
--- a/MultiplayerTestRunner/src/Utils.cpp
+++ b/MultiplayerTestRunner/src/Utils.cpp
@@ -17,50 +17,60 @@
 #include "Utils.h"
 
 #include "../include/ErrorCodes.h"
+#include "uuid_v4.h"
 
+#include "CSP/Systems/Users/UserSystem.h"
 #include <CSP/CSPFoundation.h>
-#include <CSP/Common/String.h>
+#include <CSP/Systems/SystemsManager.h>
 #include <CSP/Systems/WebService.h> //For resultbase
 #include <filesystem>
 #include <fstream>
+#include <future>
 
 namespace Utils
 {
 
-/*
- * Load the test account credentials from a location adjacent to the binary
- * Currently only provides 2 accounts, will need to be extended for tests that require more than 2 agents.
- */
-TestAccountCredentials LoadTestAccountCredentials()
+std::string Utils::GetUniqueString()
 {
-    if (!std::filesystem::exists("test_account_creds.txt"))
-    {
-        constexpr const char* msg
-            = "test_account_creds.txt not found! This file must exist and must contain the following information:\n<DefaultLoginEmail> "
-              "<DefaultLoginPassword>\n<AlternativeLoginEmail> <AlternativeLoginPassword>\n<SuperUserLoginEmail> <SuperUserLoginPassword>";
-        throw ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::COULD_NOT_FIND_CREDENTIALS_FILE, msg);
-    }
-
-    std::ifstream CredsFile;
-    CredsFile.open("test_account_creds.txt");
-
-    std::string DefaultLoginEmail, DefaultLoginPassword, AlternativeLoginEmail, AlternativeLoginPassword, SuperUserLoginEmail, SuperUserLoginPassword;
+    UUIDv4::UUIDGenerator<std::mt19937_64> uuidGenerator;
+    const UUIDv4::UUID uuid = uuidGenerator.getUUID();
 
-    CredsFile >> DefaultLoginEmail >> DefaultLoginPassword;
-    CredsFile >> AlternativeLoginEmail >> AlternativeLoginPassword;
-    CredsFile >> SuperUserLoginEmail >> SuperUserLoginPassword;
+    return uuid.str();
+}
 
-    if (DefaultLoginEmail.empty() || DefaultLoginPassword.empty() || AlternativeLoginEmail.empty() || AlternativeLoginPassword.empty()
-        || SuperUserLoginEmail.empty() || SuperUserLoginPassword.empty())
+/* Create a new user. Return the profile on success */
+csp::systems::Profile Utils::CreateTestUser(bool AgeVerified /* = true */, csp::systems::EResultCode ExpectedResultCode /* = Success */,
+    csp::systems::ERequestFailureReason ExpectedResultFailureCode /* = None */)
+{
+    auto& SystemsManager = csp::systems::SystemsManager::Get();
+    auto& UserSystem = *SystemsManager.GetUserSystem();
+
+    std::promise<csp::systems::ProfileResult> ResultPromise;
+    std::future<csp::systems::ProfileResult> ResultFuture = ResultPromise.get_future();
+
+    std::string UniqueEmail = "testnopus.pokemon+" + GetUniqueString() + "@magnopus.com";
+    UserSystem.CreateUser(nullptr, nullptr, UniqueEmail.c_str(), GeneratedTestAccountPassword, false, AgeVerified, nullptr, nullptr,
+        [&ResultPromise](csp::systems::ProfileResult Result)
+        {
+            // Callbacks are called both in progress and at the end, guard against double promise sets
+            if (Result.GetResultCode() == csp::systems::EResultCode::Success || Result.GetResultCode() == csp::systems::EResultCode::Failed)
+            {
+                ResultPromise.set_value(Result);
+            }
+        });
+
+    csp::systems::ProfileResult CreatedProfile = ResultFuture.get();
+
+    if (CreatedProfile.GetResultCode() == csp::systems::EResultCode::Success)
     {
-        constexpr const char* msg
-            = "test_account_creds.txt must be in the following format:\n<DefaultLoginEmail> <DefaultLoginPassword>\n<AlternativeLoginEmail> "
-              "<AlternativeLoginPassword>\n<SuperUserLoginEmail> <SuperUserLoginPassword>";
-        throw ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::MALFORMED_CREDENTIALS_FILE, msg);
+        return CreatedProfile.GetProfile();
+    }
+    else
+    {
+        const std::string msg = "Failed to create user, got result code " + std::to_string(static_cast<uint8_t>(CreatedProfile.GetResultCode()))
+            + "\n Response Body: " + CreatedProfile.GetResponseBody().c_str();
+        throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::FAILED_TO_CREATE_USER, msg);
     }
-
-    return TestAccountCredentials { DefaultLoginEmail.c_str(), DefaultLoginPassword.c_str(), AlternativeLoginEmail.c_str(),
-        AlternativeLoginPassword.c_str(), SuperUserLoginEmail.c_str(), SuperUserLoginPassword.c_str() };
 }
 
 void InitialiseCSPWithUserAgentInfo(const csp::common::String& EndpointRootURI)
@@ -80,4 +90,4 @@ void InitialiseCSPWithUserAgentInfo(const csp::common::String& EndpointRootURI)
     csp::CSPFoundation::SetClientUserAgentInfo(ClientHeaderInfo);
 }
 
-} // namespace Utils
\ No newline at end of file
+} // namespace Utils
diff --git a/MultiplayerTestRunner/src/Utils.h b/MultiplayerTestRunner/src/Utils.h
index c746bc756..1f599c961 100644
--- a/MultiplayerTestRunner/src/Utils.h
+++ b/MultiplayerTestRunner/src/Utils.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include <CSP/Systems/Users/Profile.h>
+
 #include <optional>
 #include <stdexcept>
 #include <string>
@@ -35,6 +37,14 @@ namespace common
 
 namespace Utils
 {
+
+std::string GetUniqueString();
+
+const char GeneratedTestAccountPassword[] = "3R{d2}3C<x[J7=jU";
+
+csp::systems::Profile CreateTestUser(bool AgeVerified = true, csp::systems::EResultCode ExpectedResultCode = csp::systems::EResultCode::Success,
+    csp::systems::ERequestFailureReason ExpectedResultFailureCode = csp::systems::ERequestFailureReason::None);
+
 // Assert a type is move capable
 #define ASSERT_MOVE_CAPABLE(Type)                                                                                                                    \
     static_assert(std::is_move_constructible_v<Type>, #Type " must be move constructible");                                                          \
@@ -55,26 +65,7 @@ struct ExceptionWithCode : public std::runtime_error
 };
 
 /*
- * Credentials struct for storing the credentials read from `test_account_creds.txt`, in the case that Login/Password are not provided via the CLI.
- */
-struct TestAccountCredentials
-{
-    std::string DefaultLoginEmail;
-    std::string DefaultLoginPassword;
-    std::string AlternativeLoginEmail;
-    std::string AlternativeLoginPassword;
-    std::string SuperUserLoginEmail;
-    std::string SuperUserLoginPassword;
-};
-ASSERT_MOVE_CAPABLE(TestAccountCredentials);
-
-/*
- * Load a `TestAccountCredentials` from `test_account_creds.txt`, in the case that Login/Password are not provided via the CLI.
- */
-TestAccountCredentials LoadTestAccountCredentials();
-
-/*
- * For the moment, this uses the known test header info so CHS dosen't reject us
+ * For the moment, this uses the known test header info so CHS doesn't reject us
  * In the future, it may become prudent to allow some or all of this information
  * to be passed as arguments.
  */
diff --git a/Tests/src/MultiplayerTestRunnerProcess.cpp b/Tests/src/MultiplayerTestRunnerProcess.cpp
index c17859b9d..4824a6585 100644
--- a/Tests/src/MultiplayerTestRunnerProcess.cpp
+++ b/Tests/src/MultiplayerTestRunnerProcess.cpp
@@ -62,8 +62,8 @@ MultiplayerTestRunnerProcess::MultiplayerTestRunnerProcess(MultiplayerTestRunner
     , ExitSpacePromise(std::exchange(other.ExitSpacePromise, std::promise<void>()))
     , LoggedOutPromise(std::exchange(other.LoggedOutPromise, std::promise<void>()))
     , TestToRun(other.TestToRun)
-    , LoginEmail(std::exchange(other.LoginEmail, std::optional<std::string>()))
-    , Password(std::exchange(other.Password, std::optional<std::string>()))
+    , LoginEmail(other.LoginEmail)
+    , Password(other.Password)
     , SpaceId(std::exchange(other.SpaceId, std::optional<std::string>()))
     , TimeoutInSeconds(std::exchange(other.TimeoutInSeconds, std::optional<int>()))
     , Endpoint(std::exchange(other.Endpoint, std::optional<std::string>()))
@@ -83,9 +83,9 @@ MultiplayerTestRunnerProcess& MultiplayerTestRunnerProcess::operator=(Multiplaye
         this->LoggedOutPromise = std::exchange(other.LoggedOutPromise, std::promise<void>());
 
         this->TestToRun = other.TestToRun;
+        this->LoginEmail = other.LoginEmail;
+        this->Password = other.Password;
 
-        this->LoginEmail = std::exchange(other.LoginEmail, std::optional<std::string>());
-        this->Password = std::exchange(other.Password, std::optional<std::string>());
         this->SpaceId = std::exchange(other.SpaceId, std::optional<std::string>());
         this->TimeoutInSeconds = std::exchange(other.TimeoutInSeconds, std::optional<int>());
         this->Endpoint = std::exchange(other.Endpoint, std::optional<std::string>());
@@ -128,8 +128,8 @@ MultiplayerTestRunnerProcess& MultiplayerTestRunnerProcess::SetEndpoint(std::str
 namespace
 {
 /* Construct the CLI arguments to pass to MultiplayerTestRunner with spawning a TinyProcessLib::Process*/
-std::vector<std::string> BuildProcessArgList(MultiplayerTestRunner::TestIdentifiers::TestIdentifier TestToRun, std::optional<std::string> LoginEmail,
-    std::optional<std::string> Password, std::optional<std::string> SpaceId, std::optional<int> TimeoutInSeconds, std::optional<std::string> Endpoint)
+std::vector<std::string> BuildProcessArgList(MultiplayerTestRunner::TestIdentifiers::TestIdentifier TestToRun, std::string LoginEmail,
+    std::string Password, std::optional<std::string> SpaceId, std::optional<int> TimeoutInSeconds, std::optional<std::string> Endpoint)
 {
     std::vector<std::string> CLIArgs;
 
@@ -138,18 +138,10 @@ std::vector<std::string> BuildProcessArgList(MultiplayerTestRunner::TestIdentifi
     CLIArgs.push_back("MultiplayerTestRunner");
     CLIArgs.push_back("--test");
     CLIArgs.push_back(MultiplayerTestRunner::TestIdentifiers::TestIdentifierToString(TestToRun));
-
-    if (LoginEmail.has_value())
-    {
-        CLIArgs.push_back("--email");
-        CLIArgs.push_back(LoginEmail.value());
-    }
-
-    if (Password.has_value())
-    {
-        CLIArgs.push_back("--password");
-        CLIArgs.push_back(Password.value());
-    }
+    CLIArgs.push_back("--email");
+    CLIArgs.push_back(LoginEmail);
+    CLIArgs.push_back("--password");
+    CLIArgs.push_back(Password);
 
     if (SpaceId.has_value())
     {
@@ -182,9 +174,9 @@ bool ContainsStr(const std::string& ContainingString, const std::string& Contain
 
 MultiplayerTestRunner::TestIdentifiers::TestIdentifier MultiplayerTestRunnerProcess::GetTestToRun() const { return TestToRun; }
 
-std::optional<std::string> MultiplayerTestRunnerProcess::GetLoginEmail() const { return LoginEmail; }
+std::string MultiplayerTestRunnerProcess::GetLoginEmail() const { return LoginEmail; }
 
-std::optional<std::string> MultiplayerTestRunnerProcess::GetPassword() const { return Password; }
+std::string MultiplayerTestRunnerProcess::GetPassword() const { return Password; }
 
 std::optional<std::string> MultiplayerTestRunnerProcess::GetSpaceId() const { return SpaceId; }
 
diff --git a/Tests/src/MultiplayerTestRunnerProcess.h b/Tests/src/MultiplayerTestRunnerProcess.h
index d6cce9cc6..8c743a847 100644
--- a/Tests/src/MultiplayerTestRunnerProcess.h
+++ b/Tests/src/MultiplayerTestRunnerProcess.h
@@ -73,8 +73,8 @@ class MultiplayerTestRunnerProcess
     /* Getters. Mostly for testing, but handy.
        Return null optionals if values have not been set. */
     MultiplayerTestRunner::TestIdentifiers::TestIdentifier GetTestToRun() const;
-    std::optional<std::string> GetLoginEmail() const;
-    std::optional<std::string> GetPassword() const;
+    std::string GetLoginEmail() const;
+    std::string GetPassword() const;
     std::optional<std::string> GetSpaceId() const;
     std::optional<int> GetTimeoutInSeconds() const;
     std::optional<std::string> GetEndpoint() const;
@@ -111,10 +111,10 @@ class MultiplayerTestRunnerProcess
 
     // The test we are telling the multiplayer test runner to invoke. Set on construction. Non-optional.
     MultiplayerTestRunner::TestIdentifiers::TestIdentifier TestToRun;
+    std::string LoginEmail;
+    std::string Password;
 
     // Optional parameters, MultiplayerTestRunner has default behaviour if not set
-    std::optional<std::string> LoginEmail;
-    std::optional<std::string> Password;
     std::optional<std::string> SpaceId;
     std::optional<int> TimeoutInSeconds;
     std::optional<std::string> Endpoint;
diff --git a/Tests/src/PublicAPITests/MultiplayerTestRunnerProcessTests.cpp b/Tests/src/PublicAPITests/MultiplayerTestRunnerProcessTests.cpp
index a7bbd1fd9..99177a1f3 100644
--- a/Tests/src/PublicAPITests/MultiplayerTestRunnerProcessTests.cpp
+++ b/Tests/src/PublicAPITests/MultiplayerTestRunnerProcessTests.cpp
@@ -18,31 +18,25 @@
 #include "TestHelpers.h"
 
 #include "gtest/gtest.h"
+#include <PublicAPITests/UserSystemTestHelpers.h>
 
 CSP_PUBLIC_TEST(CSPEngine, MultiplayerTestRunnerProcessTests, ArgTest)
 {
     MultiplayerTestRunnerProcess Process(MultiplayerTestRunner::TestIdentifiers::TestIdentifier::CREATE_AVATAR);
     EXPECT_EQ(Process.GetTestToRun(), MultiplayerTestRunner::TestIdentifiers::TestIdentifier::CREATE_AVATAR);
-    EXPECT_EQ(Process.GetInvocationArgs(), (std::vector<std::string> { "MultiplayerTestRunner", "--test", "CreateAvatar" }));
-
-    // Optional arguments have no value initially
-    EXPECT_FALSE(Process.GetLoginEmail().has_value());
-    EXPECT_FALSE(Process.GetPassword().has_value());
-    EXPECT_FALSE(Process.GetSpaceId().has_value());
-    EXPECT_FALSE(Process.GetTimeoutInSeconds().has_value());
-    EXPECT_FALSE(Process.GetEndpoint().has_value());
-
     Process.SetLoginEmail("FakeEmail@MrMoustacheMan.com");
-    EXPECT_EQ(Process.GetLoginEmail(), std::optional<std::string> { "FakeEmail@MrMoustacheMan.com" });
-    EXPECT_EQ(Process.GetInvocationArgs(),
-        (std::vector<std::string> { "MultiplayerTestRunner", "--test", "CreateAvatar", "--email", "FakeEmail@MrMoustacheMan.com" }));
-
+    EXPECT_EQ(Process.GetLoginEmail(), std::string { "FakeEmail@MrMoustacheMan.com" });
     Process.SetPassword("Hunter2");
-    EXPECT_EQ(Process.GetPassword(), std::optional<std::string> { "Hunter2" });
+    EXPECT_EQ(Process.GetPassword(), std::string { "Hunter2" });
     EXPECT_EQ(Process.GetInvocationArgs(),
         (std::vector<std::string> {
             "MultiplayerTestRunner", "--test", "CreateAvatar", "--email", "FakeEmail@MrMoustacheMan.com", "--password", "Hunter2" }));
 
+    // Optional arguments have no value initially
+    EXPECT_FALSE(Process.GetSpaceId().has_value());
+    EXPECT_FALSE(Process.GetTimeoutInSeconds().has_value());
+    EXPECT_FALSE(Process.GetEndpoint().has_value());
+
     Process.SetSpaceId("MyFakeSpaceId");
     EXPECT_EQ(Process.GetSpaceId(), std::optional<std::string> { "MyFakeSpaceId" });
     EXPECT_EQ(Process.GetInvocationArgs(),
@@ -64,8 +58,11 @@ CSP_PUBLIC_TEST(CSPEngine, MultiplayerTestRunnerProcessTests, ArgTest)
 
 CSP_PUBLIC_TEST(CSPEngine, MultiplayerTestRunnerProcessTests, FutureTest)
 {
-    // Actually invoke the runner and make sure the future's are all set
+    // Actually invoke the runner and make sure the futures are all set
     MultiplayerTestRunnerProcess Process(MultiplayerTestRunner::TestIdentifiers::TestIdentifier::CREATE_AVATAR);
+    auto TestUser = CreateTestUser();
+    Process.SetLoginEmail(TestUser.Email.c_str());
+    Process.SetPassword(GeneratedTestAccountPassword);
     Process.SetTimeoutInSeconds(0); // So we don't sit at ready for assertions for any real time.
 
     Process.StartProcess();


Reviews:
user: login: MAG-AdamThorn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T09:46:32Z
user: login: MAG-ElliotMorris
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T09:54:31Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:34:36Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:49:55Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:50:15Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:50:34Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T14:50:57Z
user: login: MAG-ElliotMorris
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:14:04Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:18:11Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T10:47:23Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:08:53Z
user: login: MAG-ElliotMorris
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:39:56Z
user: login: MAG-ChristopherAtkinson
body: Changes look good to me, glad to see credentials as mandatory arguments. 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T10:52:54Z

Review comments:
diff_hunk: @@ -35,6 +37,22 @@ namespace common
 
 namespace Utils
 {
+
+#if defined CSP_WINDOWS
+#define SPRINTF sprintf_s
+#else
+#define SPRINTF sprintf
+#endif
path: MultiplayerTestRunner/src/Utils.h
user: login: MAG-ElliotMorris
body: [Consider] Could you assemble strings without `sprintf` (use the + operator, or streams, or even bring in `libfmt`) rather than bringing this nasty cross platform switch over to the test runner?
created_at: 2025-03-19T09:23:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,17 +76,13 @@ RunnerSettings ProcessCLI(int argc, char* argv[])
     App.description(AppDescription);
 
     // These raw CLI variables populated in CLI11_PARSE, and then converted into validated and structured data below in `ValidateInvocationArgs`.
-    std::string TestIdentifier;
+    std::string TestIdentifier, LoginEmail, LoginPassword;
     std::optional<int> TimeoutInSeconds;
-    std::optional<std::string> Endpoint, LoginEmail, LoginPassword, SpaceId;
+    std::optional<std::string> Endpoint, SpaceId;
 
     App.add_option("-t,--test", TestIdentifier, "The test to run. See `include/TestIdentifiers.h` for available options.")->required();
-    App.add_option("-e,--email", LoginEmail,
-        "Login email for the test CHS account. If not set, the application will attempt to source this from a `test_account_creds.txt` "
-        "next to the binary.");
-    App.add_option("-p,--password", LoginPassword,
-        "Password for the test CHS account. If not set, the application will attempt to source this from a `test_account_creds.txt` next "
-        "to the binary.");
+    App.add_option("-e,--email", LoginEmail, "Login email for the test CHS account.`")->required();
+    App.add_option("-p,--password", LoginPassword, "Password for the test CHS account.")->required();
path: MultiplayerTestRunner/src/CLIArgs.cpp
user: login: MAG-ElliotMorris
body: [Fix] This isn't documented well enough considering the account creation behaviour. We're at least sometimes creating accounts, does this expect pre-existing accounts, or is it going to take the emails and create accounts (in which case, you also should state what happens if the accounts already exist/cannot be created)
created_at: 2025-03-19T09:29:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,11 +59,16 @@ csp::common::String LogIn(csp::systems::UserSystem& UserSystem, const csp::commo
 }
 } // namespace
 
-LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword)
+LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword, bool CreateAccount /* = true */)
 {
     auto& SystemsManager = csp::systems::SystemsManager::Get();
     auto& UserSystem = *SystemsManager.GetUserSystem();
 
+    if (CreateAccount)
+    {
+        Utils::CreateTestUser(AccountLoginEmail, AccountPassword);
+    }
path: MultiplayerTestRunner/src/LoginRAII.cpp
user: login: MAG-ElliotMorris
body: [Fix] If i'm reading this correctly this is the default?

Whilst i'm fine with this being an additional option, I don't think CSP tests should ever have the MultiplayerTestRunner create their accounts for them, simply because if we've learned anything, it's that off-process logic is hard to debug, especially over teamcity logs.

I would do 1 of 2 things

1. (better for stability & simplicity) Do not have the MultiplayerTestRunner handle creating accounts at all, make it mandatory they are passed in
2. (better for this being a utility) Provide a way in the CLI to flag that you intend to create an account, not login, but have our CSP tests always create accounts externally.

I prefer option 1, because it removes a potential error return that may popup in our tests and cause us to have to debug through, but either would be acceptable.
created_at: 2025-03-19T09:37:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,11 +59,16 @@ csp::common::String LogIn(csp::systems::UserSystem& UserSystem, const csp::commo
 }
 } // namespace
 
-LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword)
+LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword, bool CreateAccount /* = true */)
path: MultiplayerTestRunner/src/LoginRAII.cpp
user: login: MAG-ElliotMorris
body: [Fix] Needs an internal unit test testing both the `true` and `false` behaviour of `CreateAccount`.
created_at: 2025-03-19T09:39:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,11 +59,16 @@ csp::common::String LogIn(csp::systems::UserSystem& UserSystem, const csp::commo
 }
 } // namespace
 
-LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword)
+LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword, bool CreateAccount /* = true */)
path: MultiplayerTestRunner/src/LoginRAII.cpp
user: login: MAG-AdamThorn
body: [Consider] Handle create account separately so you do need to pass in an optional CreateAccount param and have the LoginRAII method also handle account creation. 
created_at: 2025-03-19T09:39:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,8 +23,8 @@
 namespace MultiplayerTestRunner::ErrorCodes
 {
 constexpr const int SUCCESS = 0;
-constexpr const int COULD_NOT_FIND_CREDENTIALS_FILE = 100;
-constexpr const int MALFORMED_CREDENTIALS_FILE = 101;
+constexpr const int FAILED_TO_CREATE_USER = 100;
+constexpr const int UNUSED_ERROR_CODE = 101;
path: MultiplayerTestRunner/include/ErrorCodes.h
user: login: MAG-ElliotMorris
body: [nit] I'd leave it as the old name, but comment it out 
```suggestion
// constexpr const int MALFORMED_CREDENTIALS_FILE = 101; --Credentials file no longer used
```
created_at: 2025-03-19T09:41:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,28 +28,19 @@ constexpr int DEFAULT_TIMEOUT_IN_SECONDS = 30;
 
 /*
  * Take the raw input from the CLI, validate, and convert to structured data.
- * If Login or Password not provided, this will attempt to read `test_account_creds.txt`, and throw if it cannot.
- * TestIdentifier is the only mandatory arg, this will validate that the string provided is an actual identifier.
+ * If Login and Password are not provided, this will throw an error.
+ * TestIdentifier is also a mandatory arg, this will validate that the string provided is an actual identifier.
  * For optional arguments, if empty data is provided, will populate them with defaults.
  * SpaceId will not be defaulted, and will be left empty if empty, in the expectation a space is about to be created and the value set externally.
  */
 CLIArgs::RunnerSettings ValidateInvocationArgs(const std::string& TestIdentifier, std::optional<std::string> LoginEmail,
     std::optional<std::string> LoginPassword, const std::optional<std::string>& Endpoint, const std::optional<std::string>& SpaceId,
     const std::optional<int>& TimeoutInSeconds)
 {
-    if (!LoginEmail.has_value() && !LoginPassword.has_value())
+    if (!LoginEmail.has_value() || !LoginPassword.has_value())
     {
-        std::cout << "Credentials not provided, attempting to find credentials file.\n";
-        Utils::TestAccountCredentials credentials = Utils::LoadTestAccountCredentials();
-        LoginEmail = credentials.DefaultLoginEmail;
-        LoginPassword = credentials.DefaultLoginPassword;
-    }
-    else if (!LoginEmail.has_value() || !LoginPassword.has_value())
-    {
-        // If only one of the email/password pair has been provided, error out entirely, it's probably a mistake
-        throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR,
-            "Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-            "wish to use the credentials file.");
+        // If only one of the email/password pair has been provided, error out
+        throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR, "Both email and password must be provided together.");
path: MultiplayerTestRunner/src/CLIArgs.cpp
user: login: MAG-ElliotMorris
body: [fix] We should update the interface types to match the new source-of-data.
Both `LoginEmail` and `LoginPassword` are mandatory now, so should not be optionals, and do not need to be validated, (as CLI11 does it for us)
created_at: 2025-03-19T09:48:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,6 +37,22 @@ namespace common
 
 namespace Utils
 {
+
+#if defined CSP_WINDOWS
+#define SPRINTF sprintf_s
+#else
+#define SPRINTF sprintf
+#endif
+
+std::string GetUniqueString();
+
+const char GeneratedTestAccountEmailFormat[] = "testnopus.pokemon+%s@magnopus.com";
+const char GeneratedTestAccountPassword[] = "3R{d2}3C<x[J7=jU";
+
+csp::systems::Profile CreateTestUser(std::string UniqueEmail, std::string Password, bool AgeVerified = true,
+    csp::systems::EResultCode ExpectedResultCode = csp::systems::EResultCode::Success,
+    csp::systems::ERequestFailureReason ExpectedResultFailureCode = csp::systems::ERequestFailureReason::None);
path: MultiplayerTestRunner/src/Utils.h
user: login: MAG-ElliotMorris
body: [Nit] Use the default arg values, rather than spelling them all out.
created_at: 2025-03-19T09:53:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,8 +23,8 @@
 namespace MultiplayerTestRunner::ErrorCodes
 {
 constexpr const int SUCCESS = 0;
-constexpr const int COULD_NOT_FIND_CREDENTIALS_FILE = 100;
-constexpr const int MALFORMED_CREDENTIALS_FILE = 101;
+constexpr const int FAILED_TO_CREATE_USER = 100;
+constexpr const int UNUSED_ERROR_CODE = 101;
path: MultiplayerTestRunner/include/ErrorCodes.h
user: login: MAG-OR
body: I think I ended up using it, so not actioning for now. will do if it turns out I don't need it
created_at: 2025-03-20T14:26:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,28 +28,19 @@ constexpr int DEFAULT_TIMEOUT_IN_SECONDS = 30;
 
 /*
  * Take the raw input from the CLI, validate, and convert to structured data.
- * If Login or Password not provided, this will attempt to read `test_account_creds.txt`, and throw if it cannot.
- * TestIdentifier is the only mandatory arg, this will validate that the string provided is an actual identifier.
+ * If Login and Password are not provided, this will throw an error.
+ * TestIdentifier is also a mandatory arg, this will validate that the string provided is an actual identifier.
  * For optional arguments, if empty data is provided, will populate them with defaults.
  * SpaceId will not be defaulted, and will be left empty if empty, in the expectation a space is about to be created and the value set externally.
  */
 CLIArgs::RunnerSettings ValidateInvocationArgs(const std::string& TestIdentifier, std::optional<std::string> LoginEmail,
     std::optional<std::string> LoginPassword, const std::optional<std::string>& Endpoint, const std::optional<std::string>& SpaceId,
     const std::optional<int>& TimeoutInSeconds)
 {
-    if (!LoginEmail.has_value() && !LoginPassword.has_value())
+    if (!LoginEmail.has_value() || !LoginPassword.has_value())
     {
-        std::cout << "Credentials not provided, attempting to find credentials file.\n";
-        Utils::TestAccountCredentials credentials = Utils::LoadTestAccountCredentials();
-        LoginEmail = credentials.DefaultLoginEmail;
-        LoginPassword = credentials.DefaultLoginPassword;
-    }
-    else if (!LoginEmail.has_value() || !LoginPassword.has_value())
-    {
-        // If only one of the email/password pair has been provided, error out entirely, it's probably a mistake
-        throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR,
-            "Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-            "wish to use the credentials file.");
+        // If only one of the email/password pair has been provided, error out
+        throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR, "Both email and password must be provided together.");
path: MultiplayerTestRunner/src/CLIArgs.cpp
user: login: MAG-OR
body: yeah I realised I removed one pair of optional keywords and forgot about 10 or more others elsewhere, and indeed this part disappears. fix to come
created_at: 2025-03-20T14:27:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,17 +76,13 @@ RunnerSettings ProcessCLI(int argc, char* argv[])
     App.description(AppDescription);
 
     // These raw CLI variables populated in CLI11_PARSE, and then converted into validated and structured data below in `ValidateInvocationArgs`.
-    std::string TestIdentifier;
+    std::string TestIdentifier, LoginEmail, LoginPassword;
     std::optional<int> TimeoutInSeconds;
-    std::optional<std::string> Endpoint, LoginEmail, LoginPassword, SpaceId;
+    std::optional<std::string> Endpoint, SpaceId;
 
     App.add_option("-t,--test", TestIdentifier, "The test to run. See `include/TestIdentifiers.h` for available options.")->required();
-    App.add_option("-e,--email", LoginEmail,
-        "Login email for the test CHS account. If not set, the application will attempt to source this from a `test_account_creds.txt` "
-        "next to the binary.");
-    App.add_option("-p,--password", LoginPassword,
-        "Password for the test CHS account. If not set, the application will attempt to source this from a `test_account_creds.txt` next "
-        "to the binary.");
+    App.add_option("-e,--email", LoginEmail, "Login email for the test CHS account.`")->required();
+    App.add_option("-p,--password", LoginPassword, "Password for the test CHS account.")->required();
path: MultiplayerTestRunner/src/CLIArgs.cpp
user: login: MAG-OR
body: Updating to: `Login email for the test CHS account. The account must already exist. If it does not exist, errors out.`
taking into account the design change mentioned below.
I'm still checking a few things before I'm ready to commit, so feel free to propose a better phrasing
created_at: 2025-03-20T14:29:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,11 +59,16 @@ csp::common::String LogIn(csp::systems::UserSystem& UserSystem, const csp::commo
 }
 } // namespace
 
-LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword)
+LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword, bool CreateAccount /* = true */)
path: MultiplayerTestRunner/src/LoginRAII.cpp
user: login: MAG-OR
body: this comment is outdated following the redesign mentioned below (the boolean no longer exists)
created_at: 2025-03-20T14:30:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,11 +59,16 @@ csp::common::String LogIn(csp::systems::UserSystem& UserSystem, const csp::commo
 }
 } // namespace
 
-LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword)
+LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword, bool CreateAccount /* = true */)
 {
     auto& SystemsManager = csp::systems::SystemsManager::Get();
     auto& UserSystem = *SystemsManager.GetUserSystem();
 
+    if (CreateAccount)
+    {
+        Utils::CreateTestUser(AccountLoginEmail, AccountPassword);
+    }
path: MultiplayerTestRunner/src/LoginRAII.cpp
user: login: MAG-OR
body: As discussed, we are going for 1. Option 2 may be considered later on, if we decide to use it as a tool. for now, there isn't really a use-case
created_at: 2025-03-20T14:31:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,6 +37,22 @@ namespace common
 
 namespace Utils
 {
+
+#if defined CSP_WINDOWS
+#define SPRINTF sprintf_s
+#else
+#define SPRINTF sprintf
+#endif
path: MultiplayerTestRunner/src/Utils.h
user: login: MAG-OR
body: addressed in 8ae555e14ff1d227b985b99e2fb9736cbe1da3d9
created_at: 2025-03-20T14:49:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,17 +76,13 @@ RunnerSettings ProcessCLI(int argc, char* argv[])
     App.description(AppDescription);
 
     // These raw CLI variables populated in CLI11_PARSE, and then converted into validated and structured data below in `ValidateInvocationArgs`.
-    std::string TestIdentifier;
+    std::string TestIdentifier, LoginEmail, LoginPassword;
     std::optional<int> TimeoutInSeconds;
-    std::optional<std::string> Endpoint, LoginEmail, LoginPassword, SpaceId;
+    std::optional<std::string> Endpoint, SpaceId;
 
     App.add_option("-t,--test", TestIdentifier, "The test to run. See `include/TestIdentifiers.h` for available options.")->required();
-    App.add_option("-e,--email", LoginEmail,
-        "Login email for the test CHS account. If not set, the application will attempt to source this from a `test_account_creds.txt` "
-        "next to the binary.");
-    App.add_option("-p,--password", LoginPassword,
-        "Password for the test CHS account. If not set, the application will attempt to source this from a `test_account_creds.txt` next "
-        "to the binary.");
+    App.add_option("-e,--email", LoginEmail, "Login email for the test CHS account.`")->required();
+    App.add_option("-p,--password", LoginPassword, "Password for the test CHS account.")->required();
path: MultiplayerTestRunner/src/CLIArgs.cpp
user: login: MAG-OR
body: addressed in 57df9a92e98fc9a533ef025faa66902ee4d94831
created_at: 2025-03-20T14:50:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,11 +59,16 @@ csp::common::String LogIn(csp::systems::UserSystem& UserSystem, const csp::commo
 }
 } // namespace
 
-LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword)
+LoginRAII::LoginRAII(const std::string& AccountLoginEmail, const std::string& AccountPassword, bool CreateAccount /* = true */)
 {
     auto& SystemsManager = csp::systems::SystemsManager::Get();
     auto& UserSystem = *SystemsManager.GetUserSystem();
 
+    if (CreateAccount)
+    {
+        Utils::CreateTestUser(AccountLoginEmail, AccountPassword);
+    }
path: MultiplayerTestRunner/src/LoginRAII.cpp
user: login: MAG-OR
body: addressed in 8ae555e14ff1d227b985b99e2fb9736cbe1da3d9
created_at: 2025-03-20T14:50:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,28 +28,19 @@ constexpr int DEFAULT_TIMEOUT_IN_SECONDS = 30;
 
 /*
  * Take the raw input from the CLI, validate, and convert to structured data.
- * If Login or Password not provided, this will attempt to read `test_account_creds.txt`, and throw if it cannot.
- * TestIdentifier is the only mandatory arg, this will validate that the string provided is an actual identifier.
+ * If Login and Password are not provided, this will throw an error.
+ * TestIdentifier is also a mandatory arg, this will validate that the string provided is an actual identifier.
  * For optional arguments, if empty data is provided, will populate them with defaults.
  * SpaceId will not be defaulted, and will be left empty if empty, in the expectation a space is about to be created and the value set externally.
  */
 CLIArgs::RunnerSettings ValidateInvocationArgs(const std::string& TestIdentifier, std::optional<std::string> LoginEmail,
     std::optional<std::string> LoginPassword, const std::optional<std::string>& Endpoint, const std::optional<std::string>& SpaceId,
     const std::optional<int>& TimeoutInSeconds)
 {
-    if (!LoginEmail.has_value() && !LoginPassword.has_value())
+    if (!LoginEmail.has_value() || !LoginPassword.has_value())
     {
-        std::cout << "Credentials not provided, attempting to find credentials file.\n";
-        Utils::TestAccountCredentials credentials = Utils::LoadTestAccountCredentials();
-        LoginEmail = credentials.DefaultLoginEmail;
-        LoginPassword = credentials.DefaultLoginPassword;
-    }
-    else if (!LoginEmail.has_value() || !LoginPassword.has_value())
-    {
-        // If only one of the email/password pair has been provided, error out entirely, it's probably a mistake
-        throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR,
-            "Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-            "wish to use the credentials file.");
+        // If only one of the email/password pair has been provided, error out
+        throw Utils::ExceptionWithCode(MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR, "Both email and password must be provided together.");
path: MultiplayerTestRunner/src/CLIArgs.cpp
user: login: MAG-OR
body: addressed in 8efa293b54eb2c3f2fd46e8643a3a0ad76f937df
created_at: 2025-03-20T14:50:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,19 +103,15 @@ TEST(CLITest, WhenNoEmailThenError)
     }
     catch (const Utils::ExceptionWithCode& Exception)
     {
-        // If there's no credentials file, we'll throw an error.
         EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR);
-        EXPECT_EQ(std::string(Exception.what()),
-            std::string("Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-                        "wish to use the credentials file."));
+        EXPECT_EQ(std::string(Exception.what()), std::string("Both email and password must be provided together."));
path: MultiplayerTestRunner/src/InternalUnitTests/Test_CLI.cpp
user: login: MAG-ElliotMorris
body: [Question] Do these still pass? From my reading of the code, this error is no longer emitted (because we don't need to do the validation anymore because these are mandatory)

If i'm right, we should [fix] this by removing the `WhenNoEmail` and `WhenNoPassword` and replacing them with tests similar to `TestIdentifierRequired`

Note: These are pure internal unit tests for development of the runner, and don't run from the main CSP test suite, you need to define `RUN_MULTIPLAYER_RUNNER_TESTS` and launch the test runner project to run these. (Maybe they should be invoked on CI though, come to think of it.)
created_at: 2025-03-21T09:13:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,19 +103,15 @@ TEST(CLITest, WhenNoEmailThenError)
     }
     catch (const Utils::ExceptionWithCode& Exception)
     {
-        // If there's no credentials file, we'll throw an error.
         EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR);
-        EXPECT_EQ(std::string(Exception.what()),
-            std::string("Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-                        "wish to use the credentials file."));
+        EXPECT_EQ(std::string(Exception.what()), std::string("Both email and password must be provided together."));
path: MultiplayerTestRunner/src/InternalUnitTests/Test_CLI.cpp
user: login: MAG-OR
body: oh I assumed all the test macros were removed and I ran everything that looked like multiplayer test runner tests from the test view in visual studio. come to think of it, I don't think these were in the list. will run...
It would be helpful to run them on the CI, I agree
created_at: 2025-03-21T09:18:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,19 +103,15 @@ TEST(CLITest, WhenNoEmailThenError)
     }
     catch (const Utils::ExceptionWithCode& Exception)
     {
-        // If there's no credentials file, we'll throw an error.
         EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR);
-        EXPECT_EQ(std::string(Exception.what()),
-            std::string("Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-                        "wish to use the credentials file."));
+        EXPECT_EQ(std::string(Exception.what()), std::string("Both email and password must be provided together."));
path: MultiplayerTestRunner/src/InternalUnitTests/Test_CLI.cpp
user: login: MAG-OR
body: yes of course it fails. will address
created_at: 2025-03-21T10:47:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,19 +103,15 @@ TEST(CLITest, WhenNoEmailThenError)
     }
     catch (const Utils::ExceptionWithCode& Exception)
     {
-        // If there's no credentials file, we'll throw an error.
         EXPECT_EQ(Exception.ErrorCode, MultiplayerTestRunner::ErrorCodes::CLI_PARSE_ERROR);
-        EXPECT_EQ(std::string(Exception.what()),
-            std::string("Both email and password must be provided together. Missing one likely indicates a mistake. Omit both if you "
-                        "wish to use the credentials file."));
+        EXPECT_EQ(std::string(Exception.what()), std::string("Both email and password must be provided together."));
path: MultiplayerTestRunner/src/InternalUnitTests/Test_CLI.cpp
user: login: MAG-OR
body: ![image](https://github.com/user-attachments/assets/d3dd0089-8009-4929-969f-086b0aa27524)

Fixed in 7a585b8bba3c2540b08f50f0fc07b08c18c5c922.

created_at: 2025-03-21T11:08:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: MAG-ElliotMorris
created_at: 2025-03-21T09:14:42Z
body: All the changes look good to me, just one thing with the internal tests given the new arrangement of mandatory CLI args.
author_association: CONTRIBUTOR
user: login: MAG-ElliotMorris
created_at: 2025-03-21T15:41:23Z
body: > oh I assumed all the test macros were removed

The reason the MultiplayerTestRunner still has a macro is because it serves as its own test project, rather than having a separate test project. Needs to be a way to tell it if it's running in test mode or not.
author_association: CONTRIBUTOR