diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 20238cb82..6a5aee8f8 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -74,6 +74,14 @@ public class LootLockerEndPoints public static EndPointClass uploadPlayerFile = new EndPointClass("player/files", LootLockerHTTPMethod.UPLOAD_FILE); public static EndPointClass updatePlayerFile = new EndPointClass("/player/files/{0}", LootLockerHTTPMethod.UPDATE_FILE); public static EndPointClass deletePlayerFile = new EndPointClass("/player/files/{0}", LootLockerHTTPMethod.DELETE); + public static EndPointClass listPlayerFileRevisions = new EndPointClass("player/files/{0}/revisions", LootLockerHTTPMethod.GET); + public static EndPointClass getPlayerFileRevision = new EndPointClass("player/files/{0}/revisions/{1}", LootLockerHTTPMethod.GET); + public static EndPointClass promotePlayerFileRevision = new EndPointClass("player/files/{0}/revisions/{1}/current", LootLockerHTTPMethod.POST); + public static EndPointClass getPlayerFileByKey = new EndPointClass("player/files/key/{0}", LootLockerHTTPMethod.GET); + public static EndPointClass listPlayerFileRevisionsByKey = new EndPointClass("player/files/key/{0}/revisions", LootLockerHTTPMethod.GET); + public static EndPointClass getPlayerFileRevisionByKey = new EndPointClass("player/files/key/{0}/revisions/{1}", LootLockerHTTPMethod.GET); + public static EndPointClass promotePlayerFileRevisionByKey = new EndPointClass("player/files/key/{0}/revisions/{1}/current", LootLockerHTTPMethod.POST); + public static EndPointClass deletePlayerFileByKey = new EndPointClass("player/files/key/{0}", LootLockerHTTPMethod.DELETE); // Player Progressions [Header("Player Progressions")] diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 10e5395f3..9571f92ed 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -4095,7 +4095,6 @@ public static void UploadPlayerFile(string pathToFile, string filePurpose, bool { "public", isPublic.ToString().ToLower() } }; - var fileBytes = new byte[] { }; try { @@ -4179,7 +4178,7 @@ public static void UploadPlayerFile(FileStream fileStream, string filePurpose, b /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void UploadPlayerFile(FileStream fileStream, string filePurpose, Action onComplete, string forPlayerWithUlid = null) { - UploadPlayerFile(fileStream, filePurpose, false, onComplete, forPlayerWithUlid); + UploadPlayerFile(fileStream, filePurpose, isPublic: false, onComplete, forPlayerWithUlid: forPlayerWithUlid); } /// @ingroup PlayerFiles @@ -4224,7 +4223,185 @@ public static void UploadPlayerFile(byte[] fileBytes, string fileName, string fi /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void UploadPlayerFile(byte[] fileBytes, string fileName, string filePurpose, Action onComplete, string forPlayerWithUlid = null) { - UploadPlayerFile(fileBytes, fileName, filePurpose, false, onComplete, forPlayerWithUlid); + UploadPlayerFile(fileBytes, fileName, filePurpose, isPublic: false, onComplete, forPlayerWithUlid: forPlayerWithUlid); + } + + /// @ingroup PlayerFiles + /////////////////////////////////////////////////////////////////////////////// + + // ================================================================ + // UploadPlayerFileByKey — dedicated overloads for upsert-by-key + // ================================================================ + + /// @ingroup PlayerFiles + /// + /// Upload a file with the provided name and content, using a key for upsert behavior. + /// If a file with the given key already exists for this player, it will be updated. + /// + /// Path to the file, example: Application.persistentDataPath + "/" + fileName; + /// Purpose of the file, example: savefile/config + /// Should this file be viewable by other players? + /// Key for upsert behavior. If a file with this key already exists, it will be updated. + /// onComplete Action for handling the response of type LootLockerPlayerFile + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void UploadPlayerFileByKey(string pathToFile, string filePurpose, bool isPublic, string key, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + var body = new Dictionary() + { + { "purpose", filePurpose }, + { "public", isPublic.ToString().ToLower() }, + { "key", key } + }; + + var fileBytes = new byte[] { }; + try + { + fileBytes = File.ReadAllBytes(pathToFile); + } + catch (Exception e) + { + LootLockerLogger.Log($"File error: {e.Message}", LootLockerLogger.LogLevel.Error); + return; + } + + LootLockerServerRequest.UploadFile(forPlayerWithUlid, LootLockerEndPoints.uploadPlayerFile, fileBytes, Path.GetFileName(pathToFile), "multipart/form-data", body, + onComplete: (serverResponse) => + { + LootLockerResponse.Deserialize(onComplete, serverResponse); + }); + } + + /// @ingroup PlayerFiles + /// + /// Upload a file with the provided name and content, using a key for upsert behavior. + /// If a file with the given key already exists for this player, it will be updated. + /// The file will not be viewable by other players. + /// + /// Path to the file, example: Application.persistentDataPath + "/" + fileName; + /// Purpose of the file, example: savefile/config + /// Key for upsert behavior. If a file with this key already exists, it will be updated. + /// onComplete Action for handling the response of type LootLockerPlayerFile + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void UploadPlayerFileByKey(string pathToFile, string filePurpose, string key, Action onComplete, string forPlayerWithUlid = null) + { + UploadPlayerFileByKey(pathToFile, filePurpose, isPublic: false, key, onComplete, forPlayerWithUlid: forPlayerWithUlid); + } + + /// @ingroup PlayerFiles + /// + /// Upload a file using a Filestream, using a key for upsert behavior. + /// If a file with the given key already exists for this player, it will be updated. + /// + /// Filestream to upload + /// Purpose of the file, example: savefile/config + /// Should this file be viewable by other players? + /// Key for upsert behavior. If a file with this key already exists, it will be updated. + /// onComplete Action for handling the response of type LootLockerPlayerFile + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void UploadPlayerFileByKey(FileStream fileStream, string filePurpose, bool isPublic, string key, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + var body = new Dictionary() + { + { "purpose", filePurpose }, + { "public", isPublic.ToString().ToLower() }, + { "key", key } + }; + + var fileBytes = new byte[fileStream.Length]; + try + { + fileStream.Read(fileBytes, 0, Convert.ToInt32(fileStream.Length)); + } + catch (Exception e) + { + LootLockerLogger.Log($"File error: {e.Message}", LootLockerLogger.LogLevel.Error); + return; + } + + LootLockerServerRequest.UploadFile(forPlayerWithUlid, LootLockerEndPoints.uploadPlayerFile, fileBytes, Path.GetFileName(fileStream.Name), "multipart/form-data", body, + onComplete: (serverResponse) => + { + LootLockerResponse.Deserialize(onComplete, serverResponse); + }); + } + + /// @ingroup PlayerFiles + /// + /// Upload a file using a Filestream, using a key for upsert behavior. + /// If a file with the given key already exists for this player, it will be updated. + /// The file will not be viewable by other players. + /// + /// Filestream to upload + /// Purpose of the file, example: savefile/config + /// Key for upsert behavior. If a file with this key already exists, it will be updated. + /// onComplete Action for handling the response of type LootLockerPlayerFile + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void UploadPlayerFileByKey(FileStream fileStream, string filePurpose, string key, Action onComplete, string forPlayerWithUlid = null) + { + UploadPlayerFileByKey(fileStream, filePurpose, isPublic: false, key, onComplete, forPlayerWithUlid: forPlayerWithUlid); + } + + /// @ingroup PlayerFiles + /// + /// Upload a file using a byte array, using a key for upsert behavior. + /// If a file with the given key already exists for this player, it will be updated. + /// + /// Byte array to upload + /// Name of the file on LootLocker + /// Purpose of the file, example: savefile/config + /// Should this file be viewable by other players? + /// Key for upsert behavior. If a file with this key already exists, it will be updated. + /// onComplete Action for handling the response of type LootLockerPlayerFile + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void UploadPlayerFileByKey(byte[] fileBytes, string fileName, string filePurpose, bool isPublic, string key, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + var body = new Dictionary() + { + { "purpose", filePurpose }, + { "public", isPublic.ToString().ToLower() }, + { "key", key } + }; + + LootLockerServerRequest.UploadFile(forPlayerWithUlid, LootLockerEndPoints.uploadPlayerFile, fileBytes, Path.GetFileName(fileName), "multipart/form-data", body, + onComplete: (serverResponse) => + { + LootLockerResponse.Deserialize(onComplete, serverResponse); + }); + } + + /// @ingroup PlayerFiles + /// + /// Upload a file using a byte array, using a key for upsert behavior. + /// If a file with the given key already exists for this player, it will be updated. + /// The file will not be viewable by other players. + /// + /// Byte array to upload + /// Name of the file on LootLocker + /// Purpose of the file, example: savefile/config + /// Key for upsert behavior. If a file with this key already exists, it will be updated. + /// onComplete Action for handling the response of type LootLockerPlayerFile + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void UploadPlayerFileByKey(byte[] fileBytes, string fileName, string filePurpose, string key, Action onComplete, string forPlayerWithUlid = null) + { + UploadPlayerFileByKey(fileBytes, fileName, filePurpose, isPublic: false, key, onComplete, forPlayerWithUlid: forPlayerWithUlid); } /// @ingroup PlayerFiles @@ -4345,6 +4522,154 @@ public static void DeletePlayerFile(int fileId, Action onCom LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.DELETE, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } + + /// @ingroup PlayerFiles + /// + /// List all revisions for a player file. + /// + /// Id of the file. + /// onComplete Action for handling the response of type LootLockerPlayerFileRevisionsResponse + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void GetPlayerFileRevisions(int fileId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.ListPlayerFileRevisions(forPlayerWithUlid, fileId, onComplete); + } + + /// @ingroup PlayerFiles + /// + /// Get a specific revision of a player file by its revision id. + /// + /// Id of the file. + /// The ULID of the revision to retrieve. + /// onComplete Action for handling the response of type LootLockerPlayerFileContent + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void GetPlayerFileRevision(int fileId, string revisionId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.GetPlayerFileRevision(forPlayerWithUlid, fileId, revisionId, onComplete); + } + + /// @ingroup PlayerFiles + /// + /// Promote a specific revision to be the current (active) revision of a player file. + /// + /// Id of the file. + /// The ULID of the revision to promote. + /// onComplete Action for handling the response of type LootLockerResponse + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void PromotePlayerFileRevision(int fileId, string revisionId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.PromotePlayerFileRevision(forPlayerWithUlid, fileId, revisionId, onComplete); + } + + /// @ingroup PlayerFiles + /// + /// Get a player file by its key. + /// + /// The key of the file. + /// onComplete Action for handling the response of type LootLockerPlayerFile + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void GetPlayerFileByKey(string key, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.GetPlayerFileByKey(forPlayerWithUlid, key, onComplete); + } + + /// @ingroup PlayerFiles + /// + /// List all revisions for a player file identified by its key. + /// + /// The key of the file. + /// onComplete Action for handling the response of type LootLockerPlayerFileRevisionsResponse + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void GetPlayerFileRevisionsByKey(string key, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.ListPlayerFileRevisionsByKey(forPlayerWithUlid, key, onComplete); + } + + /// @ingroup PlayerFiles + /// + /// Get a specific revision of a player file by its key and revision id. + /// + /// The key of the file. + /// The ULID of the revision to retrieve. + /// onComplete Action for handling the response of type LootLockerPlayerFileContent + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void GetPlayerFileRevisionByKey(string key, string revisionId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.GetPlayerFileRevisionByKey(forPlayerWithUlid, key, revisionId, onComplete); + } + + /// @ingroup PlayerFiles + /// + /// Promote a specific revision to be the current (active) revision of a player file identified by its key. + /// + /// The key of the file. + /// The ULID of the revision to promote. + /// onComplete Action for handling the response of type LootLockerResponse + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void PromotePlayerFileRevisionByKey(string key, string revisionId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.PromotePlayerFileRevisionByKey(forPlayerWithUlid, key, revisionId, onComplete); + } + + /// @ingroup PlayerFiles + /// + /// Delete a player file by its key. + /// + /// The key of the file to delete. + /// onComplete Action for handling the response of type LootLockerResponse + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void DeletePlayerFileByKey(string key, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.DeletePlayerFileByKey(forPlayerWithUlid, key, onComplete); + } #endregion #region Player progressions diff --git a/Runtime/Game/Requests/PlayerRequest.cs b/Runtime/Game/Requests/PlayerRequest.cs index 22783a442..3680fa732 100644 --- a/Runtime/Game/Requests/PlayerRequest.cs +++ b/Runtime/Game/Requests/PlayerRequest.cs @@ -380,6 +380,8 @@ public class LootLockerPlayerFile : LootLockerResponse public string revision_id { get; set; } /// The file name. public string name { get; set; } + /// The optional key for upsert operations. + public string key { get; set; } /// The file size in bytes. public int size { get; set; } /// The purpose or category tag for this file. @@ -400,6 +402,53 @@ public class LootLockerPlayerFile : LootLockerResponse public DateTime created_at { get; set; } } + /// + /// Response containing a list of revisions for a player file. + /// + public class LootLockerPlayerFileRevisionsResponse : LootLockerResponse + { + /// The list of revisions. + public LootLockerPlayerFileContent[] revisions { get; set; } + /// Metadata about the file. + public LootLockerPlayerFileMetadata file { get; set; } + /// The ULID of the current (active) revision. + public string current_revision_id { get; set; } + } + + /// + /// Metadata about a player file, returned as part of the revisions response. + /// + public class LootLockerPlayerFileMetadata + { + /// When the file was created. + public DateTime created_at { get; set; } + /// The file name. + public string name { get; set; } + /// The optional key for upsert operations. + public string key { get; set; } + /// The purpose or category tag for this file. + public string purpose { get; set; } + /// The unique identifier of this player file. + public int id { get; set; } + /// Whether this file is publicly accessible. + public bool is_public { get; set; } + } + + /// + /// A single file revision with download URL and metadata. + /// + public class LootLockerPlayerFileContent : LootLockerResponse + { + /// The ULID of this revision. + public string id { get; set; } + /// The signed URL to download this revision. + public string url { get; set; } + /// The file size in bytes. + public int size { get; set; } + /// When this revision was created. + public DateTime created_at { get; set; } + } + /// /// Response containing asset reward notifications for the current player. /// @@ -470,5 +519,53 @@ public static void LookupPlayer1stPartyPlatformIDs(string forPlayerWithUlid, Loo LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } + + public static void ListPlayerFileRevisions(string forPlayerWithUlid, int fileId, Action onComplete) + { + var endpoint = LootLockerEndPoints.listPlayerFileRevisions.WithPathParameter(fileId); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void GetPlayerFileRevision(string forPlayerWithUlid, int fileId, string revisionId, Action onComplete) + { + var endpoint = LootLockerEndPoints.getPlayerFileRevision.WithPathParameters(fileId, revisionId); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void PromotePlayerFileRevision(string forPlayerWithUlid, int fileId, string revisionId, Action onComplete) + { + var endpoint = LootLockerEndPoints.promotePlayerFileRevision.WithPathParameters(fileId, revisionId); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.POST, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void GetPlayerFileByKey(string forPlayerWithUlid, string key, Action onComplete) + { + var endpoint = LootLockerEndPoints.getPlayerFileByKey.WithPathParameter(key); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void ListPlayerFileRevisionsByKey(string forPlayerWithUlid, string key, Action onComplete) + { + var endpoint = LootLockerEndPoints.listPlayerFileRevisionsByKey.WithPathParameter(key); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void GetPlayerFileRevisionByKey(string forPlayerWithUlid, string key, string revisionId, Action onComplete) + { + var endpoint = LootLockerEndPoints.getPlayerFileRevisionByKey.WithPathParameters(key, revisionId); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void PromotePlayerFileRevisionByKey(string forPlayerWithUlid, string key, string revisionId, Action onComplete) + { + var endpoint = LootLockerEndPoints.promotePlayerFileRevisionByKey.WithPathParameters(key, revisionId); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.POST, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void DeletePlayerFileByKey(string forPlayerWithUlid, string key, Action onComplete) + { + var endpoint = LootLockerEndPoints.deletePlayerFileByKey.WithPathParameter(key); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.DELETE, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } } } diff --git a/Runtime/Game/Requests/RemoteSessionRequest.cs b/Runtime/Game/Requests/RemoteSessionRequest.cs index e536b8374..efec97937 100644 --- a/Runtime/Game/Requests/RemoteSessionRequest.cs +++ b/Runtime/Game/Requests/RemoteSessionRequest.cs @@ -56,12 +56,17 @@ public class LootLockerLeaseRemoteSessionRequest /// The Game Version configured for the game /// public string game_version { get; set; } + /// + /// Optional list of identity providers to restrict the remote session to (e.g., "steam", "apple") + /// + public string[] providers { get; set; } - public LootLockerLeaseRemoteSessionRequest(string titleId, string environmentId) + public LootLockerLeaseRemoteSessionRequest(string titleId, string environmentId, string[] providers = null) { title_id = titleId; environment_id = environmentId; game_version = LootLockerConfig.current.game_version; + this.providers = providers; } } @@ -379,7 +384,7 @@ protected IEnumerator ContinualPollingAction(Guid processGuid) { yield break; } - yield return new WaitForSeconds(preProcess.PollingIntervalSeconds); + yield return new WaitForSecondsRealtime(preProcess.PollingIntervalSeconds); while (_remoteSessionsProcesses.TryGetValue(processGuid, out var process)) { // Check if we should continue the polling @@ -418,13 +423,21 @@ protected IEnumerator ContinualPollingAction(Guid processGuid) yield break; } + // If the process was cancelled while the HTTP poll was in-flight, skip + // the status-update callback and let the next while-iteration handle it + // via the ShouldCancel check at the top of the loop. + if (processAfterStatusCheck.ShouldCancel) + { + continue; + } + if (!startSessionResponse.success) { if (startSessionResponse.statusCode >= 500 && startSessionResponse.statusCode <= 599 && processAfterStatusCheck.Retries <= _leasingProcessPollingRetryLimit) { // Recoverable error processAfterStatusCheck.Retries++; - yield return new WaitForSeconds(processAfterStatusCheck.PollingIntervalSeconds); + yield return new WaitForSecondsRealtime(processAfterStatusCheck.PollingIntervalSeconds); continue; } @@ -451,7 +464,7 @@ protected IEnumerator ContinualPollingAction(Guid processGuid) processAfterStatusCheck.LastUpdatedStatus = pollingResponse.lease_status; // Sleep for a bit before checking again - yield return new WaitForSeconds(processAfterStatusCheck.PollingIntervalSeconds); + yield return new WaitForSecondsRealtime(processAfterStatusCheck.PollingIntervalSeconds); } } @@ -549,29 +562,18 @@ private void LeaseRemoteSession( Action onComplete, string providerUrlParam = null) { + string[] providers = string.IsNullOrEmpty(providerUrlParam) ? null : new[] { providerUrlParam }; LootLockerLeaseRemoteSessionRequest leaseRemoteSessionRequest = - new LootLockerLeaseRemoteSessionRequest(titleId, environmentId); + new LootLockerLeaseRemoteSessionRequest(titleId, environmentId, providers); EndPointClass endPoint = leaseIntent == LootLockerRemoteSessionLeaseIntent.login ? LootLockerEndPoints.leaseRemoteSession : LootLockerEndPoints.leaseRemoteSessionForLinking; + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint, endPoint.httpMethod, LootLockerJson.SerializeObject(leaseRemoteSessionRequest), (serverResponse) => { var response = LootLockerResponse.Deserialize(serverResponse); - if (!string.IsNullOrEmpty(providerUrlParam) && response != null) - { - if (response.redirect_url != null) - { - string separator = response.redirect_url.Contains("?") ? "&" : "?"; - response.redirect_url = response.redirect_url + separator + "provider=" + providerUrlParam; - } - if (response.display_url != null) - { - string separator = response.display_url.Contains("?") ? "&" : "?"; - response.display_url = response.display_url + separator + "provider=" + providerUrlParam; - } - } onComplete?.Invoke(response); }, leaseIntent == LootLockerRemoteSessionLeaseIntent.link); diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationTitleConfig.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationTitleConfig.cs index 6d5dc6d66..751990bd3 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationTitleConfig.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationTitleConfig.cs @@ -8,7 +8,8 @@ public static class LootLockerTestConfigurationTitleConfig public enum TitleConfigKeys { - global_player_presence + global_player_presence, + white_label_custom_signup_fields } public class PresenceTitleConfigRequest @@ -17,6 +18,21 @@ public class PresenceTitleConfigRequest public bool advanced_mode { get; set; } } + public class WhiteLabelCustomSignUpFieldDefinition + { + public string question_text { get; set; } + public string metadata_key { get; set; } + public string field_type { get; set; } + public bool required { get; set; } + public bool sensitive { get; set; } + public int sort_order { get; set; } + } + + public class WhiteLabelCustomSignUpFieldsConfigRequest + { + public WhiteLabelCustomSignUpFieldDefinition[] fields { get; set; } + } + public static void GetGameConfig(TitleConfigKeys ConfigKey, Action onComplete) { if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) @@ -52,5 +68,29 @@ public static void UpdateGameConfig(TitleConfigKeys ConfigKey, bool Enabled, boo onComplete?.Invoke(serverResponse); }, true); } + + public static void SetCustomSignUpFields(WhiteLabelCustomSignUpFieldDefinition[] fields, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(false, "Not logged in"); + return; + } + + var request = new WhiteLabelCustomSignUpFieldsConfigRequest + { + fields = fields + }; + string json = LootLockerJson.SerializeObject(request); + LootLockerTestGameAdmin.SetGameConfig("white_label_custom_signup_fields", json, response => + { + if (response == null) + { + onComplete?.Invoke(false, "Null response from SetGameConfig"); + return; + } + onComplete?.Invoke(response.success, response.errorData?.message); + }); + } } } diff --git a/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs b/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs index 24b94a55f..7e2b1e1f2 100644 --- a/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs +++ b/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs @@ -6,7 +6,6 @@ using System; using System.Collections; using System.IO; -using System.Net; using UnityEngine; using UnityEngine.TestTools; @@ -78,10 +77,7 @@ public IEnumerator Setup() }); yield return new WaitUntil(() => guestLoginCompleted); - - Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####"); - } [UnityTearDown] @@ -110,17 +106,28 @@ public IEnumerator TearDown() Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } + // --- Helpers --- - [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + private string CreateTempFile(string content, string suffix = ".txt") + { + string path = Application.temporaryCachePath + $"/{this.GetType().Name}{TestCounter}-{Guid.NewGuid()}{suffix}"; + using (TextWriter writer = new StreamWriter(path)) + { + writer.WriteLine(content); + } + return path; + } + + // ================================================================ + // Phase 1: Core Upload & Key Tests + // ================================================================ + + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator PlayerFiles_UploadSimplePublicFile_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); // Given - string path = Application.temporaryCachePath + "/PlayerFileCanBeCreatedWithPathUpdatedAndThenDeleted-creation.txt"; - string content = "First added line"; - TextWriter writer = new StreamWriter(path); - writer.WriteLine(content); - writer.Close(); + string path = CreateTempFile("First added line"); // When LootLockerPlayerFile actualResponse = new LootLockerPlayerFile(); @@ -132,7 +139,6 @@ public IEnumerator PlayerFiles_UploadSimplePublicFile_Succeeds() playerFileUploadCompleted = true; }); - // Wait for response yield return new WaitUntil(() => playerFileUploadCompleted); // Then @@ -140,5 +146,835 @@ public IEnumerator PlayerFiles_UploadSimplePublicFile_Succeeds() Assert.Greater(actualResponse.size, 0, "File Size was 0"); Assert.AreEqual(setToPublic, actualResponse.is_public, "File does not have the same public setting"); } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_UploadWithKey_ReturnsKeyInResponse() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string path = CreateTempFile("Content with key"); + string fileKey = "test-key-" + TestCounter; + + // When + LootLockerPlayerFile actualResponse = new LootLockerPlayerFile(); + bool completed = false; + LootLockerSDKManager.UploadPlayerFileByKey(path, "test", true, fileKey, fileResponse => + { + actualResponse = fileResponse; + completed = true; + }); + + yield return new WaitUntil(() => completed); + + // Then + Assert.IsTrue(actualResponse.success, "File upload with key failed"); + Assert.AreEqual(fileKey, actualResponse.key, "Key in response does not match"); + Assert.Greater(actualResponse.size, 0, "File Size was 0"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_UploadWithSameKeyTwice_UpdatesExistingFile() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string fileKey = "upsert-key-" + TestCounter; + string pathA = CreateTempFile("Original content"); + string pathB = CreateTempFile("Updated content that is longer"); + + // When — first upload + LootLockerPlayerFile firstResponse = new LootLockerPlayerFile(); + bool firstDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, fileResponse => + { + firstResponse = fileResponse; + firstDone = true; + }); + yield return new WaitUntil(() => firstDone); + Assert.IsTrue(firstResponse.success, "First upload failed"); + + // When — second upload with same key + LootLockerPlayerFile secondResponse = new LootLockerPlayerFile(); + bool secondDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, fileResponse => + { + secondResponse = fileResponse; + secondDone = true; + }); + yield return new WaitUntil(() => secondDone); + + // Then + Assert.IsTrue(secondResponse.success, "Second upload (upsert) failed"); + Assert.AreEqual(firstResponse.id, secondResponse.id, "File ID should be the same after upsert"); + Assert.AreNotEqual(firstResponse.size, secondResponse.size, "File size should differ after upsert with different content"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_UploadWithoutKey_ReturnsEmptyKey() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string path = CreateTempFile("No key content"); + + // When + LootLockerPlayerFile actualResponse = new LootLockerPlayerFile(); + bool completed = false; + LootLockerSDKManager.UploadPlayerFile(path, "test", true, fileResponse => + { + actualResponse = fileResponse; + completed = true; + }); + + yield return new WaitUntil(() => completed); + + // Then + Assert.IsTrue(actualResponse.success, "File upload without key failed"); + Assert.IsTrue(string.IsNullOrEmpty(actualResponse.key), "Key should be null or empty when not provided"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_UploadPrivateFile_Succeeds() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string path = CreateTempFile("Private content"); + + // When + LootLockerPlayerFile actualResponse = new LootLockerPlayerFile(); + bool completed = false; + LootLockerSDKManager.UploadPlayerFile(path, "test", false, fileResponse => + { + actualResponse = fileResponse; + completed = true; + }); + + yield return new WaitUntil(() => completed); + + // Then + Assert.IsTrue(actualResponse.success, "Private file upload failed"); + Assert.IsFalse(actualResponse.is_public, "File should not be public"); + } + + // ================================================================ + // Phase 2: Key-Based Lookup & Delete + // ================================================================ + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_GetFileByKey_ReturnsCorrectFile() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string fileKey = "lookup-key-" + TestCounter; + string path = CreateTempFile("Lookup by key content"); + LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile(); + bool uploadDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(path, "test", true, fileKey, fileResponse => + { + uploadedFile = fileResponse; + uploadDone = true; + }); + yield return new WaitUntil(() => uploadDone); + Assert.IsTrue(uploadedFile.success, "Upload for lookup test failed"); + + // When + LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile(); + bool fetchDone = false; + LootLockerSDKManager.GetPlayerFileByKey(fileKey, fileResponse => + { + fetchedFile = fileResponse; + fetchDone = true; + }); + yield return new WaitUntil(() => fetchDone); + + // Then + Assert.IsTrue(fetchedFile.success, "GetPlayerFileByKey failed"); + Assert.AreEqual(uploadedFile.id, fetchedFile.id, "File ID should match"); + Assert.AreEqual(fileKey, fetchedFile.key, "Key should match"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_GetFileByKey_NonExistentKey_Fails() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // When + bool preLogErrorsAsWarningsSetting = LootLockerConfig.current.logErrorsAsWarnings; + LootLockerConfig.current.logErrorsAsWarnings = true; // Suppress error logs for expected failure + LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile(); + bool fetchDone = false; + LootLockerSDKManager.GetPlayerFileByKey("nonexistent-key-" + TestCounter, fileResponse => + { + fetchedFile = fileResponse; + fetchDone = true; + }); + yield return new WaitUntil(() => fetchDone); + LootLockerConfig.current.logErrorsAsWarnings = preLogErrorsAsWarningsSetting; + + // Then + Assert.IsFalse(fetchedFile.success, "GetPlayerFileByKey should fail for non-existent key"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_DeleteFileByKey_RemovesFile() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string fileKey = "delete-key-" + TestCounter; + string path = CreateTempFile("To be deleted by key"); + LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile(); + bool uploadDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(path, "test", true, fileKey, fileResponse => + { + uploadedFile = fileResponse; + uploadDone = true; + }); + yield return new WaitUntil(() => uploadDone); + Assert.IsTrue(uploadedFile.success, "Upload for delete-by-key test failed"); + + // When — delete by key + LootLockerResponse deleteResponse = new LootLockerResponse(); + bool deleteDone = false; + LootLockerSDKManager.DeletePlayerFileByKey(fileKey, response => + { + deleteResponse = response; + deleteDone = true; + }); + yield return new WaitUntil(() => deleteDone); + + // Then — verify deletion + Assert.IsTrue(deleteResponse.success, "DeletePlayerFileByKey failed"); + + bool preLogErrorsAsWarningsSetting = LootLockerConfig.current.logErrorsAsWarnings; + LootLockerConfig.current.logErrorsAsWarnings = true; // Suppress error logs for expected failure + LootLockerPlayerFile fetchedFileResponse = new LootLockerPlayerFile(); + bool fetchDone = false; + LootLockerSDKManager.GetPlayerFileByKey(fileKey, fileResponse => + { + fetchedFileResponse = fileResponse; + fetchDone = true; + }); + yield return new WaitUntil(() => fetchDone); + Assert.IsFalse(fetchedFileResponse.success, "File should no longer exist after deletion by key"); + LootLockerConfig.current.logErrorsAsWarnings = preLogErrorsAsWarningsSetting; + } + + // ================================================================ + // Phase 3: Revisions by ID + // ================================================================ + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_UpdateFile_CreatesNewRevision() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string pathA = CreateTempFile("Original revision content"); + string pathB = CreateTempFile("Updated revision content"); + + LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile(); + bool uploadDone = false; + LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, fileResponse => + { + uploadedFile = fileResponse; + uploadDone = true; + }); + yield return new WaitUntil(() => uploadDone); + Assert.IsTrue(uploadedFile.success, "Initial upload failed"); + + // When — update the file + LootLockerPlayerFile updatedFile = new LootLockerPlayerFile(); + bool updateDone = false; + LootLockerSDKManager.UpdatePlayerFile(uploadedFile.id, pathB, fileResponse => + { + updatedFile = fileResponse; + updateDone = true; + }); + yield return new WaitUntil(() => updateDone); + Assert.IsTrue(updatedFile.success, "Update failed"); + + // Then — list revisions + LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse(); + bool revisionsDone = false; + LootLockerSDKManager.GetPlayerFileRevisions(uploadedFile.id, response => + { + revisionsResponse = response; + revisionsDone = true; + }); + yield return new WaitUntil(() => revisionsDone); + + Assert.IsTrue(revisionsResponse.success, "List revisions failed"); + Assert.GreaterOrEqual(revisionsResponse.revisions.Length, 2, "Should have at least 2 revisions after update"); + Assert.IsNotNull(revisionsResponse.current_revision_id, "Current revision ID should be set"); + Assert.AreEqual(revisionsResponse.current_revision_id, revisionsResponse.revisions[revisionsResponse.revisions.Length - 1].id, + "Current revision should be the latest"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_GetFileRevision_ReturnsSpecificRevision() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string pathA = CreateTempFile("First revision"); + string pathB = CreateTempFile("Second revision"); + + LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile(); + bool uploadDone = false; + LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, fileResponse => + { + uploadedFile = fileResponse; + uploadDone = true; + }); + yield return new WaitUntil(() => uploadDone); + Assert.IsTrue(uploadedFile.success, "Initial upload failed"); + + // Update to create a second revision + LootLockerPlayerFile updateResponse = new LootLockerPlayerFile(); + bool updateDone = false; + LootLockerSDKManager.UpdatePlayerFile(uploadedFile.id, pathB, response => + { + updateResponse = response; + updateDone = true; + }); + yield return new WaitUntil(() => updateDone); + Assert.IsTrue(updateResponse.success, "Update to create revision 2 failed"); + + // Get revision list to find the first revision ID + LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse(); + bool revisionsDone = false; + LootLockerSDKManager.GetPlayerFileRevisions(uploadedFile.id, response => + { + revisionsResponse = response; + revisionsDone = true; + }); + yield return new WaitUntil(() => revisionsDone); + Assert.IsTrue(revisionsResponse.success, "List revisions failed"); + Assert.GreaterOrEqual(revisionsResponse.revisions.Length, 2, "Should have at least 2 revisions"); + + // When — get the first (oldest) revision + string firstRevisionId = revisionsResponse.revisions[0].id; + LootLockerPlayerFileContent revisionContent = new LootLockerPlayerFileContent(); + bool getRevisionDone = false; + LootLockerSDKManager.GetPlayerFileRevision(uploadedFile.id, firstRevisionId, response => + { + revisionContent = response; + getRevisionDone = true; + }); + yield return new WaitUntil(() => getRevisionDone); + + // Then + Assert.IsTrue(revisionContent.success, "GetPlayerFileRevision failed"); + Assert.AreEqual(firstRevisionId, revisionContent.id, "Revision ID should match"); + Assert.Greater(revisionContent.size, 0, "Revision size should be > 0"); + Assert.IsFalse(string.IsNullOrEmpty(revisionContent.url), "Revision URL should not be empty"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_PromoteFileRevision_RestoresOldRevision() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string pathA = CreateTempFile("First revision content"); + string pathB = CreateTempFile("Second revision content"); + + LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile(); + bool uploadDone = false; + LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, fileResponse => + { + uploadedFile = fileResponse; + uploadDone = true; + }); + yield return new WaitUntil(() => uploadDone); + Assert.IsTrue(uploadedFile.success, "Initial upload failed"); + + // Update to create revision 2 + LootLockerPlayerFile updateResponse = new LootLockerPlayerFile(); + bool updateDone = false; + LootLockerSDKManager.UpdatePlayerFile(uploadedFile.id, pathB, response => + { + updateResponse = response; + updateDone = true; + }); + yield return new WaitUntil(() => updateDone); + Assert.IsTrue(updateResponse.success, "Update to create revision 2 failed"); + + // Get revision list to find the first revision ID + LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse(); + bool revisionsDone = false; + LootLockerSDKManager.GetPlayerFileRevisions(uploadedFile.id, response => + { + revisionsResponse = response; + revisionsDone = true; + }); + yield return new WaitUntil(() => revisionsDone); + Assert.IsTrue(revisionsResponse.success, "List revisions failed"); + string firstRevisionId = revisionsResponse.revisions[0].id; + + // When — promote the first revision back to current + LootLockerResponse promoteResponse = new LootLockerResponse(); + bool promoteDone = false; + LootLockerSDKManager.PromotePlayerFileRevision(uploadedFile.id, firstRevisionId, response => + { + promoteResponse = response; + promoteDone = true; + }); + yield return new WaitUntil(() => promoteDone); + + // Then + Assert.IsTrue(promoteResponse.success, "Promote revision failed"); + + // Verify the current revision changed + LootLockerPlayerFile refreshedFile = new LootLockerPlayerFile(); + bool refreshDone = false; + LootLockerSDKManager.GetPlayerFile(uploadedFile.id, fileResponse => + { + refreshedFile = fileResponse; + refreshDone = true; + }); + yield return new WaitUntil(() => refreshDone); + Assert.IsTrue(refreshedFile.success, "GetPlayerFile after promote failed"); + Assert.AreEqual(firstRevisionId, refreshedFile.revision_id, "Current revision should be the promoted one"); + } + + // ================================================================ + // Phase 4: Revisions by Key + // ================================================================ + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_GetFileRevisionsByKey_ReturnsRevisions() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string fileKey = "rev-key-" + TestCounter; + string pathA = CreateTempFile("Revision A by key"); + string pathB = CreateTempFile("Revision B by key"); + + // Upload with key (creates revision 1) + LootLockerPlayerFile firstUpload = new LootLockerPlayerFile(); + bool firstDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, response => + { + firstUpload = response; + firstDone = true; + }); + yield return new WaitUntil(() => firstDone); + Assert.IsTrue(firstUpload.success, "First upload for revisions by key test failed"); + + // Upsert with same key (creates revision 2) + LootLockerPlayerFile secondUpload = new LootLockerPlayerFile(); + bool secondDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, response => + { + secondUpload = response; + secondDone = true; + }); + yield return new WaitUntil(() => secondDone); + Assert.IsTrue(secondUpload.success, "Second upload for revisions by key test failed"); + + // When + LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse(); + bool revisionsDone = false; + LootLockerSDKManager.GetPlayerFileRevisionsByKey(fileKey, response => + { + revisionsResponse = response; + revisionsDone = true; + }); + yield return new WaitUntil(() => revisionsDone); + + // Then + Assert.IsTrue(revisionsResponse.success, "GetPlayerFileRevisionsByKey failed"); + Assert.GreaterOrEqual(revisionsResponse.revisions.Length, 2, "Should have at least 2 revisions"); + Assert.AreEqual(fileKey, revisionsResponse.file.key, "File metadata key should match"); + Assert.IsNotNull(revisionsResponse.current_revision_id, "Current revision ID should be set"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_GetFileRevisionByKey_ReturnsSpecificRevision() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string fileKey = "rev-get-key-" + TestCounter; + string pathA = CreateTempFile("First revision by key"); + string pathB = CreateTempFile("Second revision by key"); + + LootLockerPlayerFile firstUpload = new LootLockerPlayerFile(); + bool firstDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, response => + { + firstUpload = response; + firstDone = true; + }); + yield return new WaitUntil(() => firstDone); + Assert.IsTrue(firstUpload.success, "First upload for get revision by key test failed"); + + LootLockerPlayerFile secondUpload = new LootLockerPlayerFile(); + bool secondDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, response => + { + secondUpload = response; + secondDone = true; + }); + yield return new WaitUntil(() => secondDone); + Assert.IsTrue(secondUpload.success, "Second upload for get revision by key test failed"); + + // Get revision list to find a revision ID + LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse(); + bool revisionsDone = false; + LootLockerSDKManager.GetPlayerFileRevisionsByKey(fileKey, response => + { + revisionsResponse = response; + revisionsDone = true; + }); + yield return new WaitUntil(() => revisionsDone); + Assert.IsTrue(revisionsResponse.success, "List revisions by key failed"); + string firstRevisionId = revisionsResponse.revisions[0].id; + + // When + LootLockerPlayerFileContent revisionContent = new LootLockerPlayerFileContent(); + bool getRevisionDone = false; + LootLockerSDKManager.GetPlayerFileRevisionByKey(fileKey, firstRevisionId, response => + { + revisionContent = response; + getRevisionDone = true; + }); + yield return new WaitUntil(() => getRevisionDone); + + // Then + Assert.IsTrue(revisionContent.success, "GetPlayerFileRevisionByKey failed"); + Assert.AreEqual(firstRevisionId, revisionContent.id, "Revision ID should match"); + Assert.Greater(revisionContent.size, 0, "Revision size should be > 0"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_PromoteFileRevisionByKey_PromotesRevision() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string fileKey = "promote-key-" + TestCounter; + string pathA = CreateTempFile("First revision for promote by key"); + string pathB = CreateTempFile("Second revision for promote by key"); + + LootLockerPlayerFile firstUpload = new LootLockerPlayerFile(); + bool firstDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, response => + { + firstUpload = response; + firstDone = true; + }); + yield return new WaitUntil(() => firstDone); + Assert.IsTrue(firstUpload.success, "First upload for promote by key test failed"); + + LootLockerPlayerFile secondUpload = new LootLockerPlayerFile(); + bool secondDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, response => + { + secondUpload = response; + secondDone = true; + }); + yield return new WaitUntil(() => secondDone); + Assert.IsTrue(secondUpload.success, "Second upload for promote by key test failed"); + + // Get revision list to find the first revision ID + LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse(); + bool revisionsDone = false; + LootLockerSDKManager.GetPlayerFileRevisionsByKey(fileKey, response => + { + revisionsResponse = response; + revisionsDone = true; + }); + yield return new WaitUntil(() => revisionsDone); + Assert.IsTrue(revisionsResponse.success, "List revisions by key failed"); + string firstRevisionId = revisionsResponse.revisions[0].id; + + // When — promote the first revision + LootLockerResponse promoteResponse = new LootLockerResponse(); + bool promoteDone = false; + LootLockerSDKManager.PromotePlayerFileRevisionByKey(fileKey, firstRevisionId, response => + { + promoteResponse = response; + promoteDone = true; + }); + yield return new WaitUntil(() => promoteDone); + + // Then + Assert.IsTrue(promoteResponse.success, "PromotePlayerFileRevisionByKey failed"); + + // Verify the current revision changed + LootLockerPlayerFile refreshedFile = new LootLockerPlayerFile(); + bool refreshDone = false; + LootLockerSDKManager.GetPlayerFileByKey(fileKey, fileResponse => + { + refreshedFile = fileResponse; + refreshDone = true; + }); + yield return new WaitUntil(() => refreshDone); + Assert.IsTrue(refreshedFile.success, "GetPlayerFileByKey after promote failed"); + Assert.AreEqual(firstRevisionId, refreshedFile.revision_id, "Current revision should be the promoted one"); + } + + // ================================================================ + // Phase 5: Existing Operations Backfill + // ================================================================ + + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public IEnumerator PlayerFiles_GetPlayerFile_ReturnsCorrectFile() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string path = CreateTempFile("Get by ID content"); + LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile(); + bool uploadDone = false; + LootLockerSDKManager.UploadPlayerFile(path, "test", true, fileResponse => + { + uploadedFile = fileResponse; + uploadDone = true; + }); + yield return new WaitUntil(() => uploadDone); + Assert.IsTrue(uploadedFile.success, "Upload for get test failed"); + + // When + LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile(); + bool fetchDone = false; + LootLockerSDKManager.GetPlayerFile(uploadedFile.id, fileResponse => + { + fetchedFile = fileResponse; + fetchDone = true; + }); + yield return new WaitUntil(() => fetchDone); + + // Then + Assert.IsTrue(fetchedFile.success, "GetPlayerFile failed"); + Assert.AreEqual(uploadedFile.id, fetchedFile.id, "File ID should match"); + Assert.AreEqual(uploadedFile.name, fetchedFile.name, "File name should match"); + Assert.Greater(fetchedFile.size, 0, "File size should be > 0"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_GetAllPlayerFiles_ReturnsFiles() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given — upload two files + string pathA = CreateTempFile("First list file"); + string pathB = CreateTempFile("Second list file"); + + LootLockerPlayerFile uploadA = new LootLockerPlayerFile(); + bool uploadADone = false; + LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, response => + { + uploadA = response; + uploadADone = true; + }); + yield return new WaitUntil(() => uploadADone); + Assert.IsTrue(uploadA.success, "First upload for list test failed"); + + LootLockerPlayerFile uploadB = new LootLockerPlayerFile(); + bool uploadBDone = false; + LootLockerSDKManager.UploadPlayerFile(pathB, "test", true, response => + { + uploadB = response; + uploadBDone = true; + }); + yield return new WaitUntil(() => uploadBDone); + Assert.IsTrue(uploadB.success, "Second upload for list test failed"); + + // When + LootLockerPlayerFilesResponse listResponse = new LootLockerPlayerFilesResponse(); + bool listDone = false; + LootLockerSDKManager.GetAllPlayerFiles(response => + { + listResponse = response; + listDone = true; + }); + yield return new WaitUntil(() => listDone); + + // Then + Assert.IsTrue(listResponse.success, "GetAllPlayerFiles failed"); + Assert.GreaterOrEqual(listResponse.items.Length, 2, "Should have at least 2 files"); + foreach (var item in listResponse.items) + { + Assert.Greater(item.id, 0, "Each file should have a positive ID"); + Assert.IsFalse(string.IsNullOrEmpty(item.name), "Each file should have a name"); + Assert.IsFalse(string.IsNullOrEmpty(item.url), "Each file should have a URL"); + } + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_DeletePlayerFile_RemovesFile() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string path = CreateTempFile("To be deleted"); + LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile(); + bool uploadDone = false; + LootLockerSDKManager.UploadPlayerFile(path, "test", true, fileResponse => + { + uploadedFile = fileResponse; + uploadDone = true; + }); + yield return new WaitUntil(() => uploadDone); + Assert.IsTrue(uploadedFile.success, "Upload for delete test failed"); + + // When + LootLockerResponse deleteResponse = new LootLockerResponse(); + bool deleteDone = false; + LootLockerSDKManager.DeletePlayerFile(uploadedFile.id, response => + { + deleteResponse = response; + deleteDone = true; + }); + yield return new WaitUntil(() => deleteDone); + + // Then + Assert.IsTrue(deleteResponse.success, "DeletePlayerFile failed"); + + // Verify deletion + bool preLogErrorsAsWarningsSetting = LootLockerConfig.current.logErrorsAsWarnings; + LootLockerConfig.current.logErrorsAsWarnings = true; // Suppress error logs for expected failure + LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile(); + bool fetchDone = false; + LootLockerSDKManager.GetPlayerFile(uploadedFile.id, fileResponse => + { + fetchedFile = fileResponse; + fetchDone = true; + }); + yield return new WaitUntil(() => fetchDone); + Assert.IsFalse(fetchedFile.success, "File should no longer exist after deletion"); + LootLockerConfig.current.logErrorsAsWarnings = preLogErrorsAsWarningsSetting; + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_UpdatePlayerFile_ChangesContent() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string pathA = CreateTempFile("Original content for update"); + string pathB = CreateTempFile("Updated content for update"); + + LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile(); + bool uploadDone = false; + LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, fileResponse => + { + uploadedFile = fileResponse; + uploadDone = true; + }); + yield return new WaitUntil(() => uploadDone); + Assert.IsTrue(uploadedFile.success, "Initial upload failed"); + int originalSize = uploadedFile.size; + string originalRevisionId = uploadedFile.revision_id; + + // When + LootLockerPlayerFile updatedFile = new LootLockerPlayerFile(); + bool updateDone = false; + LootLockerSDKManager.UpdatePlayerFile(uploadedFile.id, pathB, fileResponse => + { + updatedFile = fileResponse; + updateDone = true; + }); + yield return new WaitUntil(() => updateDone); + + // Then + Assert.IsTrue(updatedFile.success, "UpdatePlayerFile failed"); + Assert.AreNotEqual(originalRevisionId, updatedFile.revision_id, "Revision ID should change after update"); + Assert.AreNotEqual(originalSize, updatedFile.size, "File size should change after update with different content"); + } + + // ================================================================ + // Phase 6: Response Field Verification + // ================================================================ + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_ListResponse_IncludesKeyField() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string fileKey = "list-key-" + TestCounter; + string path = CreateTempFile("List response key check"); + + LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile(); + bool uploadDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(path, "test", true, fileKey, response => + { + uploadedFile = response; + uploadDone = true; + }); + yield return new WaitUntil(() => uploadDone); + Assert.IsTrue(uploadedFile.success, "Upload for list response key test failed"); + + // When + LootLockerPlayerFilesResponse listResponse = new LootLockerPlayerFilesResponse(); + bool listDone = false; + LootLockerSDKManager.GetAllPlayerFiles(response => + { + listResponse = response; + listDone = true; + }); + yield return new WaitUntil(() => listDone); + + // Then + Assert.IsTrue(listResponse.success, "GetAllPlayerFiles failed"); + bool foundKey = false; + foreach (var item in listResponse.items) + { + if (item.key == fileKey) + { + foundKey = true; + break; + } + } + Assert.IsTrue(foundKey, "List response should contain an item with the uploaded key"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PlayerFiles_RevisionsResponse_FileMetadataHasKey() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + string fileKey = "meta-key-" + TestCounter; + string pathA = CreateTempFile("Metadata key revision A"); + string pathB = CreateTempFile("Metadata key revision B"); + + LootLockerPlayerFile firstUpload = new LootLockerPlayerFile(); + bool firstDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, response => + { + firstUpload = response; + firstDone = true; + }); + yield return new WaitUntil(() => firstDone); + Assert.IsTrue(firstUpload.success, "First upload for metadata key test failed"); + + LootLockerPlayerFile secondUpload = new LootLockerPlayerFile(); + bool secondDone = false; + LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, response => + { + secondUpload = response; + secondDone = true; + }); + yield return new WaitUntil(() => secondDone); + Assert.IsTrue(secondUpload.success, "Second upload for metadata key test failed"); + + // Get file ID for the ID-based revisions call + LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile(); + bool fetchDone = false; + LootLockerSDKManager.GetPlayerFileByKey(fileKey, fileResponse => + { + fetchedFile = fileResponse; + fetchDone = true; + }); + yield return new WaitUntil(() => fetchDone); + Assert.IsTrue(fetchedFile.success, "GetPlayerFileByKey failed"); + + // When — get revisions by ID + LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse(); + bool revisionsDone = false; + LootLockerSDKManager.GetPlayerFileRevisions(fetchedFile.id, response => + { + revisionsResponse = response; + revisionsDone = true; + }); + yield return new WaitUntil(() => revisionsDone); + + // Then + Assert.IsTrue(revisionsResponse.success, "GetPlayerFileRevisions failed"); + Assert.AreEqual(fileKey, revisionsResponse.file.key, "File metadata should contain the key"); + Assert.AreEqual(fetchedFile.id, revisionsResponse.file.id, "File metadata ID should match"); + Assert.IsFalse(string.IsNullOrEmpty(revisionsResponse.file.name), "File metadata should have a name"); + } } } diff --git a/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs index c501aee98..51d021dd6 100644 --- a/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs +++ b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs @@ -66,6 +66,45 @@ public IEnumerator Setup() yield break; } + // Configure custom sign-up fields on the game + bool fieldsConfigured = false; + LootLockerTestConfigurationTitleConfig.SetCustomSignUpFields( + new LootLockerTestConfigurationTitleConfig.WhiteLabelCustomSignUpFieldDefinition[] + { + new LootLockerTestConfigurationTitleConfig.WhiteLabelCustomSignUpFieldDefinition + { + question_text = "When were you born?", + metadata_key = "birth_date", + field_type = "date", + required = true, + sensitive = false, + sort_order = 1 + }, + new LootLockerTestConfigurationTitleConfig.WhiteLabelCustomSignUpFieldDefinition + { + question_text = "Do you agree to the terms?", + metadata_key = "tos_agree", + field_type = "checkbox", + required = true, + sensitive = false, + sort_order = 2 + } + }, + (success, errorMessage) => + { + if (!success) + { + Debug.LogError($"Failed to configure custom sign-up fields: {errorMessage}"); + SetupFailed = true; + } + fieldsConfigured = true; + }); + yield return new WaitUntil(() => fieldsConfigured); + if (SetupFailed) + { + yield break; + } + Assert.IsTrue(gameUnderTest?.InitializeLootLockerSDK(), "Failed to initialize LootLockerSDK"); Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####"); @@ -114,8 +153,23 @@ public IEnumerator GetSignUpFields_WithWhiteLabelEnabled_ReturnsFieldsResponse() // Then Assert.IsTrue(actualResponse.success, "GetSignUpFields returned unsuccessful: " + actualResponse.errorData?.message); - // Fields array should be present (empty if no custom fields configured on this game) Assert.IsNotNull(actualResponse.fields, "Fields array should not be null"); + Assert.AreEqual(2, actualResponse.fields.Length, "Expected 2 custom sign-up fields to be configured"); + + // Verify the configured fields round-trip correctly (order-agnostic) + var fieldsByKey = new System.Collections.Generic.Dictionary(); + foreach (var field in actualResponse.fields) + { + fieldsByKey[field.metadata_key] = field; + } + + Assert.IsTrue(fieldsByKey.ContainsKey("birth_date"), "Expected birth_date field in response"); + Assert.AreEqual("date", fieldsByKey["birth_date"].field_type, "birth_date field_type mismatch"); + Assert.AreEqual("When were you born?", fieldsByKey["birth_date"].question_text, "birth_date question_text mismatch"); + + Assert.IsTrue(fieldsByKey.ContainsKey("tos_agree"), "Expected tos_agree field in response"); + Assert.AreEqual("checkbox", fieldsByKey["tos_agree"].field_type, "tos_agree field_type mismatch"); + Assert.AreEqual("Do you agree to the terms?", fieldsByKey["tos_agree"].question_text, "tos_agree question_text mismatch"); } // Verifies serialization round-trip for the @params keyword-escaped property