From cc53349084f43d7bc09931c467db916ab57f3df1 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Wed, 20 Aug 2025 16:10:42 +0200 Subject: [PATCH 01/22] feature: Add simplified asset listing --- Runtime/Client/LootLockerEndPoints.cs | 4 +- Runtime/Game/LootLockerSDKManager.cs | 43 +++++++++++ Runtime/Game/Requests/AssetRequest.cs | 107 +++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 5 deletions(-) diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 16a6c092..bb0cdf39 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -136,12 +136,14 @@ public class LootLockerEndPoints public static EndPointClass deleteKeyValue = new EndPointClass("v1/player/storage?key={0}", LootLockerHTTPMethod.DELETE); public static EndPointClass getOtherPlayersPublicKeyValuePairs = new EndPointClass("v1/player/{0}/storage", LootLockerHTTPMethod.GET); - // Asset storage + // Assets [Header("Assets")] public static EndPointClass gettingContexts = new EndPointClass("v1/contexts", LootLockerHTTPMethod.GET); public static EndPointClass gettingAssetListWithCount = new EndPointClass("v1/assets/list?count={0}", LootLockerHTTPMethod.GET); public static EndPointClass getAssetsById = new EndPointClass("v1/assets/by/id?asset_ids={0}", LootLockerHTTPMethod.GET); public static EndPointClass gettingAllAssets = new EndPointClass("v1/assets", LootLockerHTTPMethod.GET); + + public static EndPointClass ListAssets = new EndPointClass("assets/artful-alpaca/v1", LootLockerHTTPMethod.POST); public static EndPointClass gettingAssetInformationForOneorMoreAssets = new EndPointClass("v1/asset/{0}", LootLockerHTTPMethod.GET); public static EndPointClass listingFavouriteAssets = new EndPointClass("v1/asset/favourites", LootLockerHTTPMethod.GET); public static EndPointClass addingFavouriteAssets = new EndPointClass("v1/asset/{0}/favourite", LootLockerHTTPMethod.POST); diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 1c049568..8dfbcc77 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -4729,6 +4729,49 @@ public static void GrantAssetToPlayerInventory(int assetID, int? assetVariationI } #endregion + /// + /// List assets with default parameters (no filters, first page, default page size). + /// + /// Delegate for handling the server response + /// Optional: Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListAssetsWithDefaultParameters(Action onComplete, string forPlayerWithUlid = null) + { + ListAssets(new LootLockerListAssetsRequest(), onComplete, forPlayerWithUlid: forPlayerWithUlid); + } + + /// + /// List assets with configurable response data. Use this to limit the fields returned in the response and improve performance. + /// + /// Request object with settings on what fields to include, exclude, and what assets to filter + /// Delegate for handling the server response + /// (Optional) Used together with Page to apply pagination to this Request. PerPage designates how many notifications are considered a "page". Set to 0 to not use this filter. + /// (Optional) Used together with PerPage to apply pagination to this Request. Page designates which "page" of items to fetch. Set to 0 to not use this filter. + /// Optional: Execute the Request for the specified player. If not supplied, the default player will be used. + public static void ListAssets(LootLockerListAssetsRequest Request, Action onComplete, int PerPage = 0, int Page = 0, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + if (Page > 0) + queryParams.Add("page", Page.ToString()); + if (PerPage > 0) + queryParams.Add("per_page", PerPage.ToString()); + + string endPoint = LootLockerEndPoints.ListAssets.endPoint + queryParams.Build(); + + string body = LootLockerJson.SerializeObject(Request); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint, LootLockerEndPoints.ListAssets.httpMethod, body, + (response) => + { + var parsedResponse = LootLockerResponse.Deserialize(response); + onComplete?.Invoke(parsedResponse); + }); + } #region AssetInstance /// diff --git a/Runtime/Game/Requests/AssetRequest.cs b/Runtime/Game/Requests/AssetRequest.cs index 70ccdb78..fa09b3d6 100644 --- a/Runtime/Game/Requests/AssetRequest.cs +++ b/Runtime/Game/Requests/AssetRequest.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using LootLocker.Requests; -using System.Linq; +using LootLocker.Requests; namespace LootLocker.LootLockerEnums { @@ -68,6 +65,67 @@ public static void ResetAssetCalls() } } + /// + /// Request object for listing assets with settings for what to include, exclude, and filter. + /// + public class LootLockerListAssetsRequest + { + /// Fields to include in the response. + public LootLockerAssetIncludes includes { get; set; } = new LootLockerAssetIncludes(); + /// Fields to exclude from the response. + public LootLockerAssetExcludes excludes { get; set; } = new LootLockerAssetExcludes(); + /// Filters to apply to the asset listing. + public LootLockerAssetFilters filters { get; set; } = new LootLockerAssetFilters(); + } + + /// + /// Fields to include in the asset response. + /// + public class LootLockerAssetIncludes + { + ///If set to true, response will include storage key-value pairs. + public bool storage { get; set; } = false; + ///If set to true, response will include files. + public bool files { get; set; } = false; + ///If set to true, response will include asset data entities. + public bool data_entities { get; set; } = false; + ///If set to true, response will include asset metadata. + public bool metadata { get; set; } = false; + } + + /// + /// Fields to exclude from the asset response. + /// + public class LootLockerAssetExcludes + { + ///If set to true, UGC assets authors will not be returned. + public bool authors { get; set; } = false; + } + + /// + /// Filters to apply to the asset listing. + /// + public class LootLockerAssetFilters + { + ///If set to true, response will include only UGC assets. + public bool ugc_only { get; set; } = false; + ///If provided, only the requested ids will be returned. No pagination will be attempted or respected, maximum 100 assets. + public List asset_ids { get; set; } = new List(); + } + + /// + /// Response object for listing assets from the simple asset endpoint. + /// + [Serializable] + public class LootLockerListAssetsResponse : LootLockerResponse + { + /// List of assets returned by the endpoint. + public LootLockerSimpleAsset[] assets { get; set; } + + /// Pagination data for this request + public LootLockerExtendedPagination pagination { get; set; } + } + public class LootLockerGrantAssetRequest { public int asset_id { get; set; } @@ -145,6 +203,47 @@ public class LootLockerCommonAsset : LootLockerResponse public string[] data_entities { get; set; } } + /// + /// A simplified asset object to improve performance + /// + [Serializable] + public class LootLockerSimpleAsset + { + public int asset_id { get; set; } + public string asset_uuid { get; set; } + public string asset_ulid { get; set; } + public string asset_name { get; set; } + public int context_id { get; set; } + public string context_name { get; set; } + public LootLockerSimpleAssetAuthor author { get; set; } + public LootLockerStorage[] storage { get; set; } + public LootLockerSimpleAssetFile[] files { get; set; } + public LootLockerSimpleAssetDataEntity[] data_entities { get; set; } + public LootLockerMetadataEntry[] metadata { get; set; } + } + + public class LootLockerSimpleAssetAuthor + { + public int player_id { get; set; } + public string player_ulid { get; set; } + public string public_uid { get; set; } + public string active_name { get; set; } + } + + public class LootLockerSimpleAssetFile + { + public int size { get; set; } + public string name { get; set; } + public string url { get; set; } + public string[] tags { get; set; } + } + + public class LootLockerSimpleAssetDataEntity + { + public string name { get; set; } + public string data { get; set; } + } + public class LootLockerAssetCandidate { public int created_by_player_id { get; set; } From 15b2fa06b95dfe2ee98601ab9d217445a0c06b54 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 22 Aug 2025 14:46:38 +0200 Subject: [PATCH 02/22] ci: Add tests for simplified asset listing --- Runtime/Game/Requests/AssetRequest.cs | 46 +- .../LootLockerTestConfigurationAsset.cs | 121 ++++ .../LootLockerTestConfigurationEndpoints.cs | 9 + .../LootLockerTestConfigurationMetadata.cs | 324 +++++++++++ ...ootLockerTestConfigurationMetadata.cs.meta | 2 + Tests/LootLockerTests/PlayMode/AssetTests.cs | 527 ++++++++++++++++++ .../PlayMode/AssetTests.cs.meta | 2 + 7 files changed, 1028 insertions(+), 3 deletions(-) create mode 100644 Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs create mode 100644 Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs.meta create mode 100644 Tests/LootLockerTests/PlayMode/AssetTests.cs create mode 100644 Tests/LootLockerTests/PlayMode/AssetTests.cs.meta diff --git a/Runtime/Game/Requests/AssetRequest.cs b/Runtime/Game/Requests/AssetRequest.cs index fa09b3d6..f75cfeb3 100644 --- a/Runtime/Game/Requests/AssetRequest.cs +++ b/Runtime/Game/Requests/AssetRequest.cs @@ -1,4 +1,7 @@ -using LootLocker.Requests; +using System; +using System.Collections.Generic; +using System.Linq; +using LootLocker.Requests; namespace LootLocker.LootLockerEnums { @@ -167,7 +170,44 @@ public class LootLockerFilter } [Serializable] - public class LootLockerCommonAsset : LootLockerResponse + public class LootLockerCommonAsset + { + public int id { get; set; } + public string uuid { get; set; } + public string ulid { get; set; } + public string name { get; set; } + public bool active { get; set; } + public bool purchasable { get; set; } + public string type { get; set; } + public int price { get; set; } + public int? sales_price { get; set; } + public string display_price { get; set; } + public string context { get; set; } + public string unlocks_context { get; set; } + public bool detachable { get; set; } + public string updated { get; set; } + public string marked_new { get; set; } + public int default_variation_id { get; set; } + public string description { get; set; } + public LootLockerLinks links { get; set; } + public LootLockerStorage[] storage { get; set; } + public LootLockerRarity rarity { get; set; } + public bool popular { get; set; } + public int popularity_score { get; set; } + public bool unique_instance { get; set; } + public LootLockerRental_Options[] rental_options { get; set; } + public LootLockerFilter[] filters { get; set; } + public LootLockerVariation[] variations { get; set; } + public bool featured { get; set; } + public bool context_locked { get; set; } + public bool initially_purchasable { get; set; } + public LootLockerFile[] files { get; set; } + public LootLockerAssetCandidate asset_candidate { get; set; } + public string[] data_entities { get; set; } + } + + [Serializable] + public class LootLockerCommonAssetResponse : LootLockerResponse { public int id { get; set; } public string uuid { get; set; } @@ -449,7 +489,7 @@ public static void GetAssetById(string forPlayerWithUlid, LootLockerGetRequest d })); } - public static void GetAssetInformation(string forPlayerWithUlid, LootLockerGetRequest data, Action onComplete) + public static void GetAssetInformation(string forPlayerWithUlid, LootLockerGetRequest data, Action onComplete) { EndPointClass endPoint = LootLockerEndPoints.gettingAssetInformationForOneorMoreAssets; diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationAsset.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationAsset.cs index 1516d9bc..1b2f642d 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationAsset.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationAsset.cs @@ -1,5 +1,7 @@ using LootLocker; using System; +using System.Collections.Generic; +using System.Linq; using Random = UnityEngine.Random; namespace LootLockerTestConfigurationUtils @@ -48,6 +50,48 @@ public static void CreateAsset(int contextID, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(null); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.activateAsset; + var formattedEndpoint = string.Format(endpoint.endPoint, asset_id); + + string json = LootLockerJson.SerializeObject(new LootLockerTestActivateAssetRequest()); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete, true); + } + + public static void AddDataEntityToAsset(int asset_id, string name, string data, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(null); + return; + } + + var dataEntityRequest = new LootLockerTestDataEntityRequest + { + asset_id = asset_id, + name = name, + data = data + }; + + var endpoint = LootLockerTestConfigurationEndpoints.addDataEntityToAsset; + + string json = LootLockerJson.SerializeObject(dataEntityRequest); + + LootLockerAdminRequest.Send(endpoint.endPoint, endpoint.httpMethod, json, onComplete: (serverResponse) => + { + var assetResponse = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(assetResponse); + }, true); + } + public static string GetRandomAssetName() { string[] colors = { "Green", "Blue", "Red", "Black", "Yellow", "Orange", "Purple", "Indigo", "Clear", "White", "Magenta", "Marine", "Crimson", "Teal" }; @@ -69,6 +113,45 @@ public static void CreateReward(LootLockerRewardRequest request, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(null); + return; + } + + LootLockerTestMetadata.PerformMetadataOperations(LootLockerTestMetadata.LootLockerTestMetadataSources.asset, assetUlid, new List + { + new LootLockerTestMetadata.LootLockerTestMetadataOperation + { + action = LootLockerTestMetadata.LootLockerTestMetadataActions.upsert, + key = key, + value = value, + type = LootLockerTestMetadata.LootLockerTestMetadataTypes.String, + tags = new[] { "test", "asset", "metadata" }, + access = new [] {"game_api.read"}, + + } + }, onComplete); + } + + public static void UpdateAsset(string assetJson, int assetId, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(null); + return; + } + + var formatted = string.Format(LootLockerTestConfigurationEndpoints.updateAsset.endPoint, assetId); + + LootLockerAdminRequest.Send(formatted, LootLockerTestConfigurationEndpoints.updateAsset.httpMethod, assetJson, onComplete: (updateResponse) => + { + onComplete?.Invoke(updateResponse); + }, true); + } } public class LootLockerTestAssetResponse : LootLockerResponse @@ -84,6 +167,29 @@ public class LootLockerTestAsset public string name { get; set; } } + public class LootLockerTestAssetContext + { + public int id { get; set; } + public string name { get; set; } + } + + public class LootLockerTestCompleteAsset : LootLocker.Requests.LootLockerCommonAsset + { + public bool global { get; set; } + public int context_id { get; set; } + public LootLockerTestAssetContext[] contexts { get; set; } + } + + public class LootLockerTestSingleAssetResponse : LootLockerResponse + { + public LootLockerTestCompleteAsset asset { get; set; } + } + + public class LootLockerTestActivateAssetRequest + { + public bool live_and_dev { get; set; } = true; + } + public class LootLockerRewardResponse : LootLockerResponse { public string id { get; set; } @@ -95,11 +201,26 @@ public class LootLockerRewardRequest public string entity_kind { get; set; } } + public class LootLockerTestDataEntityRequest + { + public int asset_id { get; set; } + public string name { get; set; } + public string data { get; set; } + } + public class LootLockerTestContextResponse : LootLockerResponse { public LootLockerTestContext[] contexts { get; set; } } + public class LootLockerTestDataEntityResponse : LootLockerResponse + { + public int id { get; set; } + public string name { get; set; } + public string data { get; set; } + public string updated_at { get; set; } + } + public class LootLockerTestContext { public int id { get; set; } diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs index 8612eea1..3ecb0681 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs @@ -90,6 +90,12 @@ public class LootLockerTestConfigurationEndpoints [Header("LootLocker Admin API Asset Operations")] public static EndPointClass getAssetContexts = new EndPointClass("/v1/game/#GAMEID#/assets/contexts", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); public static EndPointClass createAsset = new EndPointClass("/v1/game/#GAMEID#/asset", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass updateAsset = new EndPointClass("/v1/game/#GAMEID#/asset/{0}", LootLockerHTTPMethod.PATCH, LootLockerCallerRole.Admin); + public static EndPointClass getAsset = new EndPointClass("/v1/game/#GAMEID#/asset/{0}", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); + + public static EndPointClass activateAsset = new EndPointClass("/v1/game/#GAMEID#/asset/{0}/activate", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass addDataEntityToAsset = new EndPointClass("/v1/game/#GAMEID#/data", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass updateAssetFilters = new EndPointClass("/v1/game/#GAMEID#/assets/filters/bulk", LootLockerHTTPMethod.PATCH, LootLockerCallerRole.Admin); public static EndPointClass createReward = new EndPointClass("game/#GAMEID#/reward", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); [Header("LootLocker Admin API Trigger Operations")] @@ -97,6 +103,9 @@ public class LootLockerTestConfigurationEndpoints [Header("LootLocker Admin API Notification Operations")] public static EndPointClass sendCustomNotification = new EndPointClass("game/#GAMEID#/notifications/v1", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + + [Header("LootLocker Admin API Metadata Operations")] + public static EndPointClass metadataOperations = new EndPointClass("game/#GAMEID#/metadata", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); } #endregion } diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs new file mode 100644 index 00000000..3618917c --- /dev/null +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs @@ -0,0 +1,324 @@ + +using System; +using System.Collections.Generic; +using LootLocker; + +namespace LootLockerTestConfigurationUtils +{ + public class LootLockerTestMetadata + { + + public static void PerformMetadataOperations(LootLockerTestMetadataSources Source, string SourceID, List OperationsToPerform, Action onComplete) + { + List entries = new List(); + foreach (var op in OperationsToPerform) + { + entries.Add(new LootLockerTestInternalMetadataOperationWithStringEnums(op)); + } + + LootLockerTestInternalMetadataOperationRequest request = new LootLockerTestInternalMetadataOperationRequest + { + source = Source.ToString().ToLower(), + source_id = SourceID, + entries = entries.ToArray() + }; + + string json = LootLockerJson.SerializeObject(request); + + LootLockerAdminRequest.Send(LootLockerTestConfigurationEndpoints.metadataOperations.endPoint, LootLockerEndPoints.metadataOperations.httpMethod, json, (serverResponse) => + { + LootLockerResponse.Deserialize(onComplete, serverResponse); + }, true); + } + + /// + /// Possible metadata sources + /// + public enum LootLockerTestMetadataSources + { + reward = 0, + leaderboard = 1, + catalog_item = 2, + progression = 3, + currency = 4, + player = 5, + self = 6, + asset = 7, + }; + + /// + /// Possible metadata types + /// + public enum LootLockerTestMetadataTypes + { + String = 0, + Number = 1, + Bool = 2, + Json = 3, + Base64 = 4, + }; + + /// + /// Possible metadata actions + /// + public enum LootLockerTestMetadataActions + { + create = 0, + update = 1, + delete = 2, + create_or_update = 3, // Alias for upsert (same thing) + upsert = 4, + }; + + /// + /// + public class LootLockerTestMetadataBase64Value + { + /// + /// The type of content that the base64 string encodes. Could be for example "image/jpeg" if it is a base64 encoded jpeg, or "application/x-redacted" if loading of files has been disabled + /// + public string content_type { get; set; } + /// + /// The encoded content in the form of a Base64 String. If this is unexpectedly empty, check if Content_type is set to "application/x-redacted". If it is, then the request for metadata was made with the ignoreFiles parameter set to true + /// + public string content { get; set; } + + } + + /// + /// + public class LootLockerTestMetadataEntry + { + /// + /// The value of the metadata in base object format (unparsed). To use this as the type specified by the Type field, parse it using the corresponding TryGetValueAs method in C++ or using the LootLockerTestMetadataValueParser node in Blueprints. + /// + public object value { get; set; } + /// + /// The metadata key + /// + public string key { get; set; } + /// + /// The type of value this metadata contains. Use this to know how to parse the value. + /// + public LootLockerTestMetadataTypes type { get; set; } + /// + /// List of tags applied to this metadata entry + /// + public string[] tags { get; set; } + /// + /// The access level set for this metadata entry. Valid values are game_api.read, game_api.write and player.read (only applicable for player metadata and means that the metadata entry is readable for players except the owner), though no values are required. + /// Note that different sources can allow or disallow a subset of these values. + /// + public string[] access { get; set; } + /// + /// Get the value as a String. Returns true if value could be parsed in which case output contains the value in string format, returns false if parsing failed. + /// + public bool TryGetValueAsString(out string output) + { + output = value.ToString(); + return true; + } + /// + /// Get the value as a double. Returns true if value could be parsed in which case output contains the value in double format, returns false if parsing failed which can happen if the value is not numeric, the conversion under or overflows, or the string value precision is larger than can be dealt within a double. + /// + public bool TryGetValueAsDouble(out double output) + { + try + { + string doubleAsString = value.ToString(); + return double.TryParse(doubleAsString, out output); + } + catch (InvalidCastException) + { + output = 0.0; + return false; + } + } + /// + /// Get the value as an integer. Returns true if value could be parsed in which case output contains the value in integer format, returns false if parsing failed which can happen if the value is not numeric or the conversion under or overflows + /// + public bool TryGetValueAsInteger(out int output) + { + try + { + string intAsString = value.ToString(); + return int.TryParse(intAsString, out output); + } + catch (InvalidCastException) + { + output = 0; + return false; + } + } + /// + /// Get the value as a boolean. Returns true if value could be parsed in which case output contains the value in boolean format, returns false if parsing failed which can happen if the string is not a convertible to a boolean. + /// + public bool TryGetValueAsBool(out bool output) + { + try + { + string boolAsString = value.ToString(); + return bool.TryParse(boolAsString, out output); + } + catch (InvalidCastException) + { + output = false; + return false; + } + } + + /// + /// Get the value as the specified type. Returns true if value could be parsed in which case output contains the parsed object, returns false if parsing failed which can happen if the value is not a valid json object string convertible to the specified object. + /// + public bool TryGetValueAsType(out T output) + { + return LootLockerJson.TryDeserializeObject(LootLockerJson.SerializeObject(value), out output); + } + +#if LOOTLOCKER_USE_NEWTONSOFTJSON + /// + /// Get the value as a Json Object. Returns true if value could be parsed in which case output contains the value in Json Object format, returns false if parsing failed which can happen if the value is not a valid json object string. + /// + public bool TryGetValueAsJson(out JObject output) + { + return TryGetValueAsType(out output); + } + + /// + /// Get the value as a Json Array. Returns true if value could be parsed in which case output contains the value in Json Array format, returns false if parsing failed which can happen if the value is not a valid json array string. + /// + public bool TryGetValueAsJsonArray(out JArray output) + { + output = JArray.Parse(value.ToString()); + return output != null; + } +#else + + /// + /// Get the value as a Json Object (a dictionary of string keys to object values). Returns true if value could be parsed in which case output contains the value in Json Object format, returns false if parsing failed which can happen if the value is not a valid json object string. + /// + public bool TryGetValueAsJson(out Dictionary output) + { + return TryGetValueAsType(out output) && output != null; + } + + /// + /// Get the value as a Json Array. Returns true if value could be parsed in which case output contains the value in Json Array format, returns false if parsing failed which can happen if the value is not a valid json array string. + /// + public bool TryGetValueAsJsonArray(out object[] output) + { + if (value.GetType() == typeof(object[])) + { + output = (object[])value; + return true; + } + return LootLockerJson.TryDeserializeObject(value.ToString(), out output); + } +#endif + /// + /// Get the value as a LootLockerTestMetadataBase64Value object. Returns true if value could be parsed in which case output contains the FLootLockerTestMetadataBase64Value, returns false if parsing failed. + /// + public bool TryGetValueAsBase64(out LootLockerTestMetadataBase64Value output) + { + return TryGetValueAsType(out output); + } + } + + /// + /// + public class LootLockerTestMetadataOperation : LootLockerTestMetadataEntry + { + /// + /// The type of action to perform for this metadata operation + /// + public LootLockerTestMetadataActions action { get; set; } + } + + /// + /// + public class LootLockerTestInternalMetadataOperationWithStringEnums : LootLockerTestMetadataOperation + { + public LootLockerTestInternalMetadataOperationWithStringEnums(LootLockerTestMetadataOperation other) + { + this.key = other.key; + this.type = other.type.ToString().ToLower(); + this.value = other.value; + this.tags = other.tags; + this.access = other.access; + this.action = other.action == LootLockerTestMetadataActions.create_or_update ? "upsert" : other.action.ToString().ToLower(); + } + + public new string type { get; set; } + public new string action { get; set; } + } + + /// + /// + public class LootLockerTestInternalMetadataOperationRequest + { + /// + /// The type of source that the source id refers to + /// + public string source { get; set; } + /// + /// The id of the specific source that the set operation was taken on, note that if source is set to self then this should also be set to "self" + /// + public string source_id { get; set; } + /// + /// List of operations to perform for the given source + /// + public LootLockerTestInternalMetadataOperationWithStringEnums[] entries { get; set; } + } + + public class LootLockerTestMetadataOperationErrorKeyTypePair + { + /// + /// The metadata key that the operation error refers to + /// + public string key { get; set; } + /// + /// The type of value that the set operation was for + /// + public LootLockerTestMetadataTypes type { get; set; } + } + + /// + /// + public class LootLockerTestMetadataOperationError + { + /// + /// The type of action that this metadata operation was + /// + public LootLockerTestMetadataActions action { get; set; } + /// + /// The error message describing why this metadata set operation failed + /// + public string error { get; set; } + /// + /// The key and type of value that the operation was for + /// + public LootLockerTestMetadataOperationErrorKeyTypePair entry { get; set; } + } + + /// + /// + public class LootLockerTestMetadataOperationsResponse : LootLockerResponse + { + /// + /// The type of source that the source id refers to + /// + public LootLockerTestMetadataSources source { get; set; } + /// + /// The id of the specific source that the set operation was taken on, note that if source is set to self then this will also be set to "self" + /// + public string source_id { get; set; } + /// + /// A list of errors (if any) that occurred when executing the provided metadata actions + /// + public LootLockerTestMetadataOperationError[] errors { get; set; } + } + + } + + +} \ No newline at end of file diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs.meta b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs.meta new file mode 100644 index 00000000..072fe027 --- /dev/null +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dcf423e9b65e5614ab9d30cfbefbb0ce \ No newline at end of file diff --git a/Tests/LootLockerTests/PlayMode/AssetTests.cs b/Tests/LootLockerTests/PlayMode/AssetTests.cs new file mode 100644 index 00000000..124bf035 --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/AssetTests.cs @@ -0,0 +1,527 @@ +using System.Collections; +using System.Collections.Generic; +using LootLocker; +using LootLocker.Requests; +using LootLockerTestConfigurationUtils; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; +using UnityEngine.TestTools; + +namespace LootLockerTests.PlayMode +{ + public class AssetTests + { + // Setup and teardown similar to LeaderboardTest + private LootLockerTestGame gameUnderTest = null; + private LootLockerConfig configCopy = null; + private static int TestCounter = 0; + private bool SetupFailed = false; + private int numberOfAssetsToCreate = 5; + private int numberOfAssetDataEntitiesToAdd = 7; + private int numberOfAssetMetadataToAdd = 8; + private int numberOfAssetStorageKeysToAdd = 4; + private List createdAssetIds = new List(); + private List createdAssetUlids = new List(); + + [UnitySetUp] + public IEnumerator Setup() + { + TestCounter++; + createdAssetIds.Clear(); + createdAssetUlids.Clear(); + configCopy = LootLockerConfig.current; + Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} setup #####"); + + if (!LootLockerConfig.ClearSettings()) + { + Debug.LogError("Could not clear LootLocker config"); + } + + // Create game + bool gameCreationCallCompleted = false; + LootLockerTestGame.CreateGame(testName: this.GetType().Name + TestCounter + " ", onComplete: (success, errorMessage, game) => + { + if (!success) + { + gameCreationCallCompleted = true; + Debug.LogError(errorMessage); + SetupFailed = true; + } + gameUnderTest = game; + gameCreationCallCompleted = true; + }); + yield return new WaitUntil(() => gameCreationCallCompleted); + if (SetupFailed) + { + yield break; + } + gameUnderTest?.SwitchToStageEnvironment(); + + // Enable guest platform + bool enableGuestLoginCallCompleted = false; + gameUnderTest?.EnableGuestLogin((success, errorMessage) => + { + if (!success) + { + Debug.LogError(errorMessage); + SetupFailed = true; + } + enableGuestLoginCallCompleted = true; + }); + yield return new WaitUntil(() => enableGuestLoginCallCompleted); + if (SetupFailed) + { + yield break; + } + Assert.IsTrue(gameUnderTest?.InitializeLootLockerSDK(), "Successfully created test game and initialized LootLocker"); + + bool getAssetContextsCallCompleted = false; + int contextId = 0; + LootLockerTestAssets.GetAssetContexts((success, errorMessage, contextResponse) => + { + if (!success) + { + Debug.LogError(errorMessage); + SetupFailed = true; + } + contextId = contextResponse?.contexts?[0].id ?? 0; + getAssetContextsCallCompleted = true; + }); + + yield return new WaitUntil(() => getAssetContextsCallCompleted); + if (SetupFailed) + { + yield break; + } + + for (int i = 0; i < numberOfAssetsToCreate; i++) + { + bool assetCreationCallCompleted = false; + LootLockerTestAssets.CreateAsset(contextId, (assetResponse) => + { + if (assetResponse == null || !assetResponse.success) + { + Debug.LogError("Failed to create asset: " + assetResponse?.errorData?.message); + SetupFailed = true; + assetCreationCallCompleted = true; + } + else + { + createdAssetIds.Add(assetResponse.asset.id); + createdAssetUlids.Add(assetResponse.asset.ulid); + LootLockerTestAssets.ActivateAsset(assetResponse.asset.id, (activateResponse) => + { + if (activateResponse == null || !activateResponse.success) + { + Debug.LogError("Failed to activate asset: " + activateResponse?.errorData?.message); + SetupFailed = true; + } + + string stringAsset = assetResponse.text; + stringAsset = stringAsset.Replace("{\"success\":true,\"asset\":", ""); + stringAsset = stringAsset.Remove(stringAsset.Length - 1); + string storageArrayString = "\"storage\": ["; + for (int j = 0; j < numberOfAssetStorageKeysToAdd; j++) + { + if (j > 0) + { + storageArrayString += ","; + } + storageArrayString += "{\"key\":\"storage_key_" + j + "\",\"value\":\"storage_value_" + j + "\"}"; + } + storageArrayString += "]"; + + stringAsset = stringAsset.Replace("\"storage\":[]", storageArrayString); + LootLockerTestAssets.UpdateAsset(stringAsset, assetResponse.asset.id, (updateResponse) => + { + if (updateResponse == null || !updateResponse.success) + { + Debug.LogError("Failed to update asset: " + updateResponse?.errorData?.message); + SetupFailed = true; + } + assetCreationCallCompleted = true; + }); + }); + } + }); + yield return new WaitUntil(() => assetCreationCallCompleted); + if (SetupFailed) + { + yield break; + } + } + + for (int i = 0; i < createdAssetIds.Count; i++) + { + for (int j = 0; j < numberOfAssetDataEntitiesToAdd; j++) + { + bool addDataEntityCallCompleted = false; + LootLockerTestAssets.AddDataEntityToAsset(createdAssetIds[i], "data_entity_" + j, "data_value_" + j, (response) => + { + if (response == null || !response.success) + { + Debug.LogError("Failed to add data entity to asset: " + response?.errorData?.message); + SetupFailed = true; + } + addDataEntityCallCompleted = true; + }); + yield return new WaitUntil(() => addDataEntityCallCompleted); + if (SetupFailed) + { + yield break; + } + } + + for (int j = 0; j < numberOfAssetMetadataToAdd; j++) + { + bool addMetadataCallCompleted = false; + LootLockerTestAssets.AddMetadataToAsset(createdAssetUlids[i], "metadata_key_" + j, "metadata_value_" + j, (response) => + { + if (response == null || !response.success) + { + Debug.LogError("Failed to add metadata to asset: " + response?.errorData?.message); + SetupFailed = true; + } + addMetadataCallCompleted = true; + }); + yield return new WaitUntil(() => addMetadataCallCompleted); + if (SetupFailed) + { + yield break; + } + } + } + + // Sign in client + bool guestLoginCompleted = false; + LootLockerSDKManager.StartGuestSession(GUID.Generate().ToString(), response => + { + SetupFailed |= !response.success; + guestLoginCompleted = true; + }); + yield return new WaitUntil(() => guestLoginCompleted); + + Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####"); + } + + [UnityTearDown] + public IEnumerator TearDown() + { + Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} test case #####"); + if (gameUnderTest != null) + { + bool gameDeletionCallCompleted = false; + gameUnderTest.DeleteGame(((success, errorMessage) => + { + if (!success) + { + Debug.LogError(errorMessage); + } + + gameUnderTest = null; + gameDeletionCallCompleted = true; + })); + yield return new WaitUntil(() => gameDeletionCallCompleted); + } + + LootLockerStateData.ClearAllSavedStates(); + + LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, + configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); + } + + [UnityTest, Category("LootLocker")] + [Timeout(360_000)] + public IEnumerator ListAssets_DefaultParameters_ReturnsAssetsWithNullValuesForIncludables() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + // Set up in setup + + // When + bool listAssetsCallCompleted = false; + LootLockerListAssetsResponse listResponse = null; + + LootLockerSDKManager.ListAssetsWithDefaultParameters((assetsResponse) => + { + listResponse = assetsResponse; + listAssetsCallCompleted = true; + }); + yield return new WaitUntil(() => listAssetsCallCompleted); + + // Then + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListAssets call failed"); + Assert.IsNotEmpty(listResponse.assets, "Assets list should not be empty"); + Assert.AreEqual(listResponse.pagination.total, numberOfAssetsToCreate, "Total assets count does not match the number created"); + foreach (var asset in listResponse.assets) + { + Assert.IsNotNull(asset.asset_id, "Asset ID should not be null"); + Assert.IsNotNull(asset.asset_ulid, "Asset ULID should not be null"); + Assert.IsNotNull(asset.asset_name, "Asset name should not be null"); + Assert.IsNull(asset.metadata, "Asset metadata should be null"); + Assert.IsEmpty(asset.data_entities, "Asset data entities should be null"); + Assert.IsEmpty(asset.storage, "Asset storage should be null"); + Assert.IsNull(asset.author, "Asset author should be null"); + } + } + + [UnityTest, Category("LootLocker")] + [Timeout(360_000)] + public IEnumerator ListAssets_IncludeStorage_ReturnsAssetsWithStorageButNullOtherwise() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + // Set up in setup + + // When + bool listAssetsCallCompleted = false; + LootLockerListAssetsResponse listResponse = null; + + LootLockerSDKManager.ListAssets(new LootLocker.Requests.LootLockerListAssetsRequest + { + includes = new LootLockerAssetIncludes + { + storage = true + } + }, (assetsResponse) => + { + listResponse = assetsResponse; + listAssetsCallCompleted = true; + }); + yield return new WaitUntil(() => listAssetsCallCompleted); + + // Then + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListAssets call failed"); + Assert.IsNotEmpty(listResponse.assets, "Assets list should not be empty"); + Assert.AreEqual(listResponse.pagination.total, numberOfAssetsToCreate, "Total assets count does not match the number created"); + foreach (var asset in listResponse.assets) + { + Assert.IsNotNull(asset.asset_id, "Asset ID should not be null"); + Assert.IsNotNull(asset.asset_ulid, "Asset ULID should not be null"); + Assert.IsNotNull(asset.asset_name, "Asset name should not be null"); + Assert.IsNull(asset.metadata, "Asset metadata should be null"); + Assert.IsEmpty(asset.data_entities, "Asset data entities should be null"); + Assert.IsNotEmpty(asset.storage, "Asset storage should not be empty"); + Assert.AreEqual(numberOfAssetStorageKeysToAdd, asset.storage.Length, "Asset storage count does not match the number added"); + Assert.IsNull(asset.author, "Asset author should be null"); + } + } + + [UnityTest, Category("LootLocker")] + [Timeout(360_000)] + public IEnumerator ListAssets_IncludeMetadata_ReturnsAssetsWithMetadataButNullOtherwise() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + // Set up in setup + + // When + bool listAssetsCallCompleted = false; + LootLockerListAssetsResponse listResponse = null; + + LootLockerSDKManager.ListAssets(new LootLocker.Requests.LootLockerListAssetsRequest + { + includes = new LootLockerAssetIncludes + { + metadata = true + } + }, (assetsResponse) => + { + listResponse = assetsResponse; + listAssetsCallCompleted = true; + }); + yield return new WaitUntil(() => listAssetsCallCompleted); + + // Then + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListAssets call failed"); + Assert.IsNotEmpty(listResponse.assets, "Assets list should not be empty"); + Assert.AreEqual(listResponse.pagination.total, numberOfAssetsToCreate, "Total assets count does not match the number created"); + foreach (var asset in listResponse.assets) + { + Assert.IsNotNull(asset.asset_id, "Asset ID should not be null"); + Assert.IsNotNull(asset.asset_ulid, "Asset ULID should not be null"); + Assert.IsNotNull(asset.asset_name, "Asset name should not be null"); + Assert.IsNotEmpty(asset.metadata, "Asset metadata should not be empty"); + Assert.AreEqual(numberOfAssetMetadataToAdd, asset.metadata.Length, "Asset metadata count does not match the number added"); + Assert.IsEmpty(asset.data_entities, "Asset data entities should be null"); + Assert.IsEmpty(asset.storage, "Asset storage should be null"); + Assert.IsNull(asset.author, "Asset author should be null"); + } + } + + [UnityTest, Category("LootLocker")] + [Timeout(360_000)] + public IEnumerator ListAssets_IncludeDataEntities_ReturnsAssetsWithDataEntitiesButNullOtherwise() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + // Set up in setup + + // When + bool listAssetsCallCompleted = false; + LootLockerListAssetsResponse listResponse = null; + + LootLockerSDKManager.ListAssets(new LootLocker.Requests.LootLockerListAssetsRequest + { + includes = new LootLockerAssetIncludes + { + data_entities = true + } + }, (assetsResponse) => + { + listResponse = assetsResponse; + listAssetsCallCompleted = true; + }); + yield return new WaitUntil(() => listAssetsCallCompleted); + + // Then + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListAssets call failed"); + Assert.IsNotEmpty(listResponse.assets, "Assets list should not be empty"); + Assert.AreEqual(listResponse.pagination.total, numberOfAssetsToCreate, "Total assets count does not match the number created"); + foreach (var asset in listResponse.assets) + { + Assert.IsNotNull(asset.asset_id, "Asset ID should not be null"); + Assert.IsNotNull(asset.asset_ulid, "Asset ULID should not be null"); + Assert.IsNotNull(asset.asset_name, "Asset name should not be null"); + Assert.IsNull(asset.metadata, "Asset metadata should be null"); + Assert.IsNotEmpty(asset.data_entities, "Asset data entities should not be null"); + Assert.AreEqual(numberOfAssetDataEntitiesToAdd, asset.data_entities.Length, "Asset data entities count does not match the number added"); + Assert.IsEmpty(asset.storage, "Asset storage should be null"); + Assert.IsNull(asset.author, "Asset author should be null"); + } + } + + [UnityTest, Category("LootLocker")] + [Timeout(360_000)] + public IEnumerator ListAssets_IncludeEverything_ReturnsAssetsWithAllIncludables() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + // Set up in setup + + // When + bool listAssetsCallCompleted = false; + LootLockerListAssetsResponse listResponse = null; + + LootLockerSDKManager.ListAssets(new LootLocker.Requests.LootLockerListAssetsRequest + { + includes = new LootLockerAssetIncludes + { + data_entities = true, + storage = true, + metadata = true, + files = true + } + }, (assetsResponse) => + { + listResponse = assetsResponse; + listAssetsCallCompleted = true; + }); + yield return new WaitUntil(() => listAssetsCallCompleted); + + // Then + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListAssets call failed"); + Assert.IsNotEmpty(listResponse.assets, "Assets list should not be empty"); + Assert.AreEqual(listResponse.pagination.total, numberOfAssetsToCreate, "Total assets count does not match the number created"); + foreach (var asset in listResponse.assets) + { + Assert.IsNotNull(asset.asset_id, "Asset ID should not be null"); + Assert.IsNotNull(asset.asset_ulid, "Asset ULID should not be null"); + Assert.IsNotNull(asset.asset_name, "Asset name should not be null"); + Assert.IsNotNull(asset.metadata, "Asset metadata should not be null"); + Assert.AreEqual(numberOfAssetMetadataToAdd, asset.metadata.Length, "Asset metadata count does not match the number added"); + Assert.IsNotEmpty(asset.data_entities, "Asset data entities should not be null"); + Assert.AreEqual(numberOfAssetDataEntitiesToAdd, asset.data_entities.Length, "Asset data entities count does not match the number added"); + Assert.IsNotEmpty(asset.storage, "Asset storage should not be null"); + Assert.AreEqual(numberOfAssetStorageKeysToAdd, asset.storage.Length, "Asset storage count does not match the number added"); + Assert.IsNull(asset.author, "Asset author should be null"); + } + } + + [UnityTest, Category("LootLocker")] + [Timeout(360_000)] + public IEnumerator ListAssets_WithPaginationParameters_ReturnsExpectedAsset() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + // Set up in setup + bool listAssetsCallCompleted = false; + LootLockerListAssetsResponse listResponse = null; + + LootLockerSDKManager.ListAssets(new LootLocker.Requests.LootLockerListAssetsRequest { }, (assetsResponse) => + { + listResponse = assetsResponse; + listAssetsCallCompleted = true; + }); + yield return new WaitUntil(() => listAssetsCallCompleted); + + // When + bool listAssetsWithPaginationCallCompleted = false; + LootLockerListAssetsResponse paginatedListResponse = null; + LootLockerSDKManager.ListAssets(new LootLocker.Requests.LootLockerListAssetsRequest { }, (assetsResponse) => + { + paginatedListResponse = assetsResponse; + listAssetsWithPaginationCallCompleted = true; + }, 1, 2); + yield return new WaitUntil(() => listAssetsWithPaginationCallCompleted); + + // Then + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListAssets call failed"); + Assert.IsTrue(paginatedListResponse.success, paginatedListResponse.errorData?.ToString() ?? "Paginated ListAssets call failed"); + Assert.AreEqual(listResponse.pagination.total, paginatedListResponse.pagination.total, "Total assets count does not match"); + Assert.AreEqual(1, paginatedListResponse.assets.Length, "Paginated response should contain only one asset"); + Assert.AreEqual(listResponse.assets[2].asset_ulid, paginatedListResponse.assets[0].asset_ulid, "The expected asset was not returned in the paginated response"); + } + + [UnityTest, Category("LootLocker")] + [Timeout(360_000)] + public IEnumerator ListAssets_WithFilterAndAllIncludes_ReturnsExpectedAssetWithAllIncludes() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + // Given + // Set up in setup + + // When + bool listAssetsWithFilterCallCompleted = false; + LootLockerListAssetsResponse filteredListResponse = null; + LootLockerSDKManager.ListAssets(new LootLocker.Requests.LootLockerListAssetsRequest + { + includes = new LootLockerAssetIncludes + { + data_entities = true, + storage = true, + metadata = true, + files = true, + }, + filters = new LootLockerAssetFilters + { + asset_ids = new List { createdAssetIds[2] }, + } + }, (assetsResponse) => + { + filteredListResponse = assetsResponse; + listAssetsWithFilterCallCompleted = true; + }); + yield return new WaitUntil(() => listAssetsWithFilterCallCompleted); + + // Then + Assert.IsTrue(filteredListResponse.success, filteredListResponse.errorData?.ToString() ?? "Paginated ListAssets call failed"); + Assert.AreEqual(1, filteredListResponse.pagination.total, "Should only be 1 asset with current filter"); + Assert.AreEqual(1, filteredListResponse.assets.Length, "Paginated response should contain only one asset"); + Assert.AreEqual(createdAssetIds[2], filteredListResponse.assets[0].asset_id, "The expected asset was not returned in the paginated response"); + Assert.AreEqual(createdAssetUlids[2], filteredListResponse.assets[0].asset_ulid, "The expected asset was not returned in the paginated response"); + foreach (var asset in filteredListResponse.assets) + { + Assert.IsNotNull(asset.metadata, "Asset metadata should not be null"); + Assert.AreEqual(numberOfAssetMetadataToAdd, asset.metadata.Length, "Asset metadata count does not match the number added"); + Assert.IsNotEmpty(asset.data_entities, "Asset data entities should not be null"); + Assert.AreEqual(numberOfAssetDataEntitiesToAdd, asset.data_entities.Length, "Asset data entities count does not match the number added"); + Assert.IsNotEmpty(asset.storage, "Asset storage should not be null"); + Assert.AreEqual(numberOfAssetStorageKeysToAdd, asset.storage.Length, "Asset storage count does not match the number added"); + Assert.IsNull(asset.author, "Asset author should be null"); + } + } + } +} diff --git a/Tests/LootLockerTests/PlayMode/AssetTests.cs.meta b/Tests/LootLockerTests/PlayMode/AssetTests.cs.meta new file mode 100644 index 00000000..73c9fce7 --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/AssetTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 92eed46ae51d0c14790420778acd14a5 \ No newline at end of file From 8be74e94ea657758e8d498da4b75a4afce179b6e Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 22 Aug 2025 14:46:53 +0200 Subject: [PATCH 03/22] chore: Remove unused workflow --- .github/workflows/selfhosted-poc.yml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 .github/workflows/selfhosted-poc.yml diff --git a/.github/workflows/selfhosted-poc.yml b/.github/workflows/selfhosted-poc.yml deleted file mode 100644 index 481862c6..00000000 --- a/.github/workflows/selfhosted-poc.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Selfhosted Runner Proof of Concept -run-name: selfhosted-poc -on: - workflow_dispatch: {} - -jobs: - ping-stage-test: - name: Ping stage backend - runs-on: [ubuntu-latest] - steps: - - name: Setup Tailscale - uses: tailscale/github-action@v2 - with: - oauth-client-id: ${{ SECRETS.CI_TS_OAUTH_CLIENT_ID }} - oauth-secret: ${{ SECRETS.CI_TS_OAUTH_SECRET }} - tags: tag:ci - - name: Ping stage - run: | - curl -X POST "${{ SECRETS.LL_STAGE_URL }}/game/v2/session/guest" -H "Content-Type: application/json" -d "{\"game_key\": \"dev_dc52acb52a8b49be81761e709f1df9fd\", \"game_version\": \"0.10.0.0\", \"player_identifier\": \"k8s-runner-guest\"}" From 61d2cf9e6e8cab7a095ebb878575bd491769c681 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 22 Aug 2025 14:48:12 +0200 Subject: [PATCH 04/22] fix: Add serializability to hero responses --- Runtime/Game/Requests/HeroRequest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Runtime/Game/Requests/HeroRequest.cs b/Runtime/Game/Requests/HeroRequest.cs index f07fd9a6..76033956 100644 --- a/Runtime/Game/Requests/HeroRequest.cs +++ b/Runtime/Game/Requests/HeroRequest.cs @@ -10,17 +10,17 @@ namespace LootLocker.Requests { public class LootLockerPlayerHeroResponse : LootLockerResponse { - public LootLockerPlayerHero hero; + public LootLockerPlayerHero hero { get; set; } } public class LootLockerGameHeroResponse : LootLockerResponse { - public LootLockerHero[] game_heroes; + public LootLockerHero[] game_heroes { get; set; } } public class LootLockerListHeroResponse : LootLockerResponse { - public LootLockerPlayerHero[] heroes; + public LootLockerPlayerHero[] heroes { get; set; } } public class LootLockerHeroLoadoutResponse : LootLockerResponse From 810064c438c07a8f71442bf1f0e90f6f9ea31672 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 22 Aug 2025 14:50:38 +0200 Subject: [PATCH 05/22] ci: Add test categories and a minimal and a full ci test config --- .github/workflows/run-tests-and-package.yml | 11 +++++- .../LootLockerTests/PlayMode/FriendsTests.cs | 12 +++---- .../PlayMode/GuestSessionTest.cs | 18 +++++----- Tests/LootLockerTests/PlayMode/JsonTests.cs | 28 +++++++-------- .../PlayMode/LeaderboardTest.cs | 6 ++-- .../PlayMode/MultiUserTests.cs | 36 +++++++++---------- .../PlayMode/NotificationTests.cs | 18 +++++----- Tests/LootLockerTests/PlayMode/PingTest.cs | 10 +++--- .../PlayMode/PlayerFilesTest.cs | 2 +- .../PlayMode/PlayerInfoTest.cs | 4 +-- .../PlayMode/PlayerStorageTest.cs | 6 ++-- .../PlayMode/RateLimiterTests.cs | 22 ++++++------ .../PlayMode/SessionRefreshTest.cs | 4 +-- .../PlayMode/SubmitScoreTest.cs | 14 ++++---- .../LootLockerTests/PlayMode/TriggerTests.cs | 12 +++---- .../PlayMode/WhiteLabelLoginTest.cs | 12 +++---- .../PlayMode/leaderboardDetailsTest.cs | 4 +-- 17 files changed, 116 insertions(+), 103 deletions(-) diff --git a/.github/workflows/run-tests-and-package.yml b/.github/workflows/run-tests-and-package.yml index 08fe9074..e871bd5c 100644 --- a/.github/workflows/run-tests-and-package.yml +++ b/.github/workflows/run-tests-and-package.yml @@ -11,6 +11,7 @@ on: push: branches: # Made towards the following - main + - dev workflow_dispatch: inputs: LL_URL: @@ -485,6 +486,14 @@ jobs: - name: Cat projects settings file run: | cat TestProject/ProjectSettings/ProjectSettings.asset + - name: Set test category to full ci when PR to main + if: ${{ github.event_name == 'pull_request' && github.base_ref == 'main' }} + run: | + echo "TEST_CATEGORY=-testcategory LootLockerCI" >> $GITHUB_ENV + - name: Set test category to minimal ci + if: ${{ github.event_name != 'pull_request' || github.base_ref != 'main' }} + run: | + echo "TEST_CATEGORY=-testcategory LootLockerCIFast" >> $GITHUB_ENV ####### RUN TESTS ########### - name: Cache Libraries if: ${{ vars.ENABLE_INTEGRATION_TESTS == 'true' }} @@ -508,7 +517,7 @@ jobs: checkName: Integration tests (${{ matrix.unityVersion }}-${{ ENV.JSON_LIBRARY }}) Test Results artifactsPath: ${{ matrix.unityVersion }}-${{ ENV.JSON_LIBRARY }}-artifacts githubToken: ${{ secrets.GITHUB_TOKEN }} - customParameters: -lootlockerurl ${{ ENV.LOOTLOCKER_URL }} ${{ ENV.USER_COMMANDLINE_ARGUMENTS }} + customParameters: -lootlockerurl ${{ ENV.LOOTLOCKER_URL }} ${{ ENV.USER_COMMANDLINE_ARGUMENTS }} ${{ ENV.TEST_CATEGORY }} useHostNetwork: true ####### CLEANUP ########### - name: Bring down Go Backend diff --git a/Tests/LootLockerTests/PlayMode/FriendsTests.cs b/Tests/LootLockerTests/PlayMode/FriendsTests.cs index e430a17c..82d83aa3 100644 --- a/Tests/LootLockerTests/PlayMode/FriendsTests.cs +++ b/Tests/LootLockerTests/PlayMode/FriendsTests.cs @@ -98,7 +98,7 @@ public IEnumerator TearDown() } // This test also tests List Friends, List Outgoing requests, List Incoming requests, Send Friend Request, and Accept Friend Request in passing - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Friends_DeleteFriend_RemovesFriendFromFriendsList() { // Given @@ -217,7 +217,7 @@ public IEnumerator Friends_DeleteFriend_RemovesFriendFromFriendsList() Assert.IsFalse(foundFriendUlidPostDelete, "Friend ulid was present in friends list post delete"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Friends_DeclineIncomingFriendRequest_DoesNotAddToFriendsListAndRemovesFromIncomingAndOutgoing() { // Given @@ -330,7 +330,7 @@ public IEnumerator Friends_DeclineIncomingFriendRequest_DoesNotAddToFriendsListA Assert.IsFalse(foundFriendUlid, "Friend ulid was present in friends list"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Friends_AcceptIncomingFriendRequest_AddsToFriendsListAndRemovesFromIncomingAndOutgoing() { // Given @@ -444,7 +444,7 @@ public IEnumerator Friends_AcceptIncomingFriendRequest_AddsToFriendsListAndRemov yield break; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Friends_CancelOutgoingFriendRequest_RemovesFriendRequestFromIncomingAndOutgoingRequest() { // Given @@ -558,7 +558,7 @@ public IEnumerator Friends_CancelOutgoingFriendRequest_RemovesFriendRequestFromI yield break; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Friends_BlockPlayerWhenHavingIncomingFriendRequestThenUnblockAndReceiveNewFriendRequest_RemovesFriendRequestFromIncomingWhenBlockingAndUnblockingAllowsFriendRequestsAgain() { // Given @@ -693,7 +693,7 @@ public IEnumerator Friends_BlockPlayerWhenHavingIncomingFriendRequestThenUnblock Assert.Greater(outgoingFriendRequestsPostUnblockResponse.outgoing.Length, outgoingFriendRequestsPostBlockResponse.outgoing.Length, "Friend request was not added to outgoing when unblocking"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Friends_BlockPlayer_RemovesBlockedPlayerFromFriendsList() { // Given diff --git a/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs b/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs index 78690c63..f7d619d9 100644 --- a/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs +++ b/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs @@ -101,7 +101,7 @@ public IEnumerator TearDown() } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator StartGuestSession_WithPlayerAsIdentifier_Fails() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -129,7 +129,7 @@ public IEnumerator StartGuestSession_WithPlayerAsIdentifier_Fails() Assert.IsFalse(actualResponse.success, "Guest Session with 'player' as Identifier started despite it being disallowed"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator StartGuestSession_WithoutIdentifier_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -148,7 +148,7 @@ public IEnumerator StartGuestSession_WithoutIdentifier_Succeeds() Assert.IsFalse(string.IsNullOrEmpty(actualResponse.player_identifier), "No player_identifier found in response"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator StartGuestSession_WithProvidedIdentifier_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -173,7 +173,7 @@ public IEnumerator StartGuestSession_WithProvidedIdentifier_Succeeds() Assert.AreEqual(providedIdentifier, actualResponse.player_identifier, "response player_identifier did not match the expected identifier"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator EndGuestSession_Succeeds() { //Given @@ -199,7 +199,7 @@ public IEnumerator EndGuestSession_Succeeds() Assert.IsTrue(actualResponse.success, "GuestSession was not ended correctly"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator StartGuestSession_WithStoredIdentifier_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -233,7 +233,7 @@ public IEnumerator StartGuestSession_WithStoredIdentifier_Succeeds() Assert.AreEqual(expectedIdentifier, actualResponse.player_identifier, "Guest Session using stored Identifier failed to work"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator StartGuestSession_MultipleSessionStartsWithoutIdentifierWithDefaultPlayerActive_CreatesMultipleUsers() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -274,7 +274,7 @@ public IEnumerator StartGuestSession_MultipleSessionStartsWithoutIdentifierWithD Assert.AreNotEqual(player2Ulid, player3Ulid, "Same user created with multiple start guest session requests"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator StartGuestSession_MultipleSessionStartsWithoutIdentifierWithDefaultPlayerNotActive_ReusesDefaultUser() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -318,7 +318,7 @@ public IEnumerator StartGuestSession_MultipleSessionStartsWithoutIdentifierWithD Assert.AreEqual(player1Ulid, player3Ulid, "Default user not re-used with session request 3"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator StartGuestSession_MultipleSessionStartsWithoutIdentifierWithDefaultPlayerNotActiveButNotGuestUser_CreatesNewUser() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -368,7 +368,7 @@ public IEnumerator StartGuestSession_MultipleSessionStartsWithoutIdentifierWithD Assert.AreEqual(1, LootLockerStateData.GetActivePlayerULIDs().Count, "The expected number of players were not active"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator StartGuestSession_MultipleSessionStartsWithUlid_ReusesSpecifiedUser() { Assert.IsFalse(SetupFailed, "Failed to setup game"); diff --git a/Tests/LootLockerTests/PlayMode/JsonTests.cs b/Tests/LootLockerTests/PlayMode/JsonTests.cs index d1fd3036..23d991ab 100644 --- a/Tests/LootLockerTests/PlayMode/JsonTests.cs +++ b/Tests/LootLockerTests/PlayMode/JsonTests.cs @@ -21,7 +21,7 @@ public class MultiDimensionalArrayClass public class JsonTests { - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_DeserializingSimpleJson_Succeeds() { // Given @@ -40,7 +40,7 @@ public void Json_DeserializingSimpleJson_Succeeds() "Not deserialized, does not contain player_identifier value"); } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_DeserializingComplexArrayJson_Succeeds() { // Given @@ -61,7 +61,7 @@ public void Json_DeserializingComplexArrayJson_Succeeds() "Not deserialized, does not contain the correct character"); } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_DeserializingMultiDimensionalArray_Succeeds() { // Given @@ -82,7 +82,7 @@ public void Json_DeserializingMultiDimensionalArray_Succeeds() } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_SerializingMultidimensionalArray_Succeeds() { // Given @@ -101,7 +101,7 @@ public void Json_SerializingMultidimensionalArray_Succeeds() Assert.IsTrue(serializedJson.Contains("3-1"), "Not Serialized, does not contain 3-1 value"); } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_SerializingSimpleJson_Succeeds() { // Given @@ -144,7 +144,7 @@ public bool ShouldSerializeCompleted() } } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_ConditionalSerializationConfigured_OnlyConfiguredFieldsAreSerialized() { // Given @@ -216,7 +216,7 @@ public class CaseVariationClass public string MultiLetterISAok { get; set; } = "n/a"; }; - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_SerializationCaseConversion_CaseIsConvertedToSnake() { // Given @@ -239,7 +239,7 @@ public void Json_SerializationCaseConversion_CaseIsConvertedToSnake() #endif } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_DeserializationCaseConversion_CaseIsConvertedToSnake() { // Given @@ -265,7 +265,7 @@ public void Json_DeserializationCaseConversion_CaseIsConvertedToSnake() } #if !LOOTLOCKER_USE_NEWTONSOFTJSON - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_SimpleTypeSerialization_Succeeds() { Assert.AreEqual("true", Json.Serialize(true)); @@ -292,7 +292,7 @@ public void Json_SimpleTypeSerialization_Succeeds() Assert.AreEqual("1234.5678", Json.Serialize(1234.5678d)); } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_ListSerializationDeserialization_BackAndForthPreservesData() { var list = new List(); @@ -309,7 +309,7 @@ public void Json_ListSerializationDeserialization_BackAndForthPreservesData() Assert.AreEqual(json, json2); } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_DictionarySerializationAndDeserialization_Succeeds() { var dic = new Dictionary(); @@ -349,7 +349,7 @@ public void Json_DictionarySerializationAndDeserialization_Succeeds() Assert.AreEqual(json3, json4); } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_CyclicJsonSerialization_ThrowsCyclicJsonException() { var person = new Person { Name = "foo" }; @@ -365,7 +365,7 @@ public void Json_CyclicJsonSerialization_ThrowsCyclicJsonException() } } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_CyclicJsonSerializationWithCustomOptions_Succeeds() { var person = new Person { Name = "héllo" }; @@ -375,7 +375,7 @@ public void Json_CyclicJsonSerializationWithCustomOptions_Succeeds() Assert.IsTrue(json == "[{\"name\":\"héllo\"},{\"name\":\"héllo\"}]"); } - [Test] + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_SerializingAndDesierializingEnumArrays_Works() { // Given diff --git a/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs b/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs index c331704d..b2a3b526 100644 --- a/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs +++ b/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs @@ -132,7 +132,7 @@ public IEnumerator TearDown() Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Leaderboard_ListTopTenAsPlayer_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -192,7 +192,7 @@ public IEnumerator Leaderboard_ListTopTenAsPlayer_Succeeds() } } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Leaderboard_ListTopTenAsGeneric_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -277,7 +277,7 @@ public IEnumerator Leaderboard_ListTopTenAsGeneric_Succeeds() } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Leaderboard_ListScoresThatHaveMetadata_GetsMetadata() { Assert.IsFalse(SetupFailed, "Failed to setup game"); diff --git a/Tests/LootLockerTests/PlayMode/MultiUserTests.cs b/Tests/LootLockerTests/PlayMode/MultiUserTests.cs index 61ec4acc..a994ad9f 100644 --- a/Tests/LootLockerTests/PlayMode/MultiUserTests.cs +++ b/Tests/LootLockerTests/PlayMode/MultiUserTests.cs @@ -104,7 +104,7 @@ public IEnumerator TearDown() } //TODO: Deprecated (or rather temporary) - Remove after 20251001 - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator MultiUser_JustMigratedToMultiUserSDK_TransfersStoredUser() { // Setup Succeeded @@ -168,7 +168,7 @@ public IEnumerator MultiUser_JustMigratedToMultiUserSDK_TransfersStoredUser() Assert.False(PlayerPrefs.HasKey("LootLockerWhiteLabelSessionToken"), "Key LootLockerWhiteLabelSessionToken was not cleared from player prefs"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_MakingRequestsWithoutSpecifyingUser_UsesDefaultUser() { // Setup Succeeded @@ -203,7 +203,7 @@ public IEnumerator MultiUser_MakingRequestsWithoutSpecifyingUser_UsesDefaultUser "The expected number of local players were not 'active'"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_MakingRequestsWithSpecifiedUser_UsesSpecifiedUser() { // Setup Succeeded @@ -241,7 +241,7 @@ public IEnumerator MultiUser_MakingRequestsWithSpecifiedUser_UsesSpecifiedUser() "The expected number of local players were not 'active'"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_RequestCurrentPlayerDataForDefaultUser_GetsCorrectUserData() { // Setup Succeeded @@ -293,7 +293,7 @@ public IEnumerator MultiUser_RequestCurrentPlayerDataForDefaultUser_GetsCorrectU Assert.AreEqual(defaultPlayerData.Name, currentPlayerInfoResponse.info.name ?? ""); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_SetDefaultPlayerULID_ChangesDefaultPlayerUsedForRequests() { // Setup Succeeded @@ -377,7 +377,7 @@ public IEnumerator MultiUser_SetDefaultPlayerULID_ChangesDefaultPlayerUsedForReq Assert.AreEqual(nonDefaultPlayerData.Name, currentPlayerInfoAfterDefaultUserSwitch.info.name ?? ""); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_RequestCurrentPlayerDataForNonDefaultUser_GetsCorrectUserData() { // Setup Succeeded @@ -429,7 +429,7 @@ public IEnumerator MultiUser_RequestCurrentPlayerDataForNonDefaultUser_GetsCorre Assert.AreEqual(nonDefaultPlayerData.Name, currentPlayerInfoResponse.info.name ?? ""); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_SaveStateExistsForPlayerWhenPlayerExists_ReturnsTrue() { // Setup Succeeded @@ -464,7 +464,7 @@ public IEnumerator MultiUser_SaveStateExistsForPlayerWhenPlayerExists_ReturnsTru Assert.AreEqual(nonDefaultGuestSessionResponse.player_ulid, playerData.ULID); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_SaveStateExistsForPlayerWhenPlayerDoesNotExist_ReturnsFalse() { // Setup Succeeded @@ -485,7 +485,7 @@ public IEnumerator MultiUser_SaveStateExistsForPlayerWhenPlayerDoesNotExist_Retu yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_GetActivePlayerUlid_ListsAllActivePlayers() { // Setup Succeeded @@ -518,7 +518,7 @@ public IEnumerator MultiUser_GetActivePlayerUlid_ListsAllActivePlayers() Assert.AreEqual(guestUsersToCreate, matches); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_SetPlayerULIDToInactive_MakesThePlayerNotActiveButSaveStateStillExistsAndIsCached() { // Setup Succeeded @@ -590,7 +590,7 @@ public IEnumerator MultiUser_SetPlayerULIDToInactive_MakesThePlayerNotActiveButS } } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_ClearSavedStateForPlayer_SaveStateIsRemoved() { // Setup Succeeded @@ -622,7 +622,7 @@ public IEnumerator MultiUser_ClearSavedStateForPlayer_SaveStateIsRemoved() Assert.IsNull(LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(ulidToRemove)); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_ClearAllSavedStates_AllSaveStatesAreRemoved() { // Setup Succeeded @@ -655,7 +655,7 @@ public IEnumerator MultiUser_ClearAllSavedStates_AllSaveStatesAreRemoved() } } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_SetPlayerDataWhenPlayerExists_UpdatesPlayerCache() { // Setup Succeeded @@ -705,7 +705,7 @@ public IEnumerator MultiUser_SetPlayerDataWhenPlayerExists_UpdatesPlayerCache() Assert.AreEqual("ChangedName", postUpdatePlayerData.Name); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_SetPlayerDataWhenPlayerDoesNotExistButOtherPlayersActive_CreatesPlayerCacheButDoesNotSetDefault() { // Setup Succeeded @@ -758,7 +758,7 @@ public IEnumerator MultiUser_SetPlayerDataWhenPlayerDoesNotExistButOtherPlayersA Assert.AreEqual("ChangedName", postUpdateNewPlayerData.Name); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_SetPlayerDataWhenNoPlayerCachesExist_CreatesPlayerCacheAndSetsDefault() { // Setup Succeeded @@ -803,7 +803,7 @@ public IEnumerator MultiUser_SetPlayerDataWhenNoPlayerCachesExist_CreatesPlayerC yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_SetPlayerDataWhenPlayerCachesExistButNoPlayersAreActive_CreatesPlayerCacheAndSetsDefault() { // Setup Succeeded @@ -851,7 +851,7 @@ public IEnumerator MultiUser_SetPlayerDataWhenPlayerCachesExistButNoPlayersAreAc yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_GetPlayerUlidFromWLEmailWhenPlayerIsCached_ReturnsCorrectULID() { // Setup Succeeded @@ -900,7 +900,7 @@ public IEnumerator MultiUser_GetPlayerUlidFromWLEmailWhenPlayerIsCached_ReturnsC yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator MultiUser_GetPlayerUlidFromWLEmailWhenPlayerIsNotCached_ReturnsNoULID() { // Setup Succeeded diff --git a/Tests/LootLockerTests/PlayMode/NotificationTests.cs b/Tests/LootLockerTests/PlayMode/NotificationTests.cs index d420f136..e1820025 100644 --- a/Tests/LootLockerTests/PlayMode/NotificationTests.cs +++ b/Tests/LootLockerTests/PlayMode/NotificationTests.cs @@ -207,7 +207,7 @@ private IEnumerator CreateTriggerWithReward(string triggerKey, string triggerNam onComplete?.Invoke(triggerCreated, errorMessage, createdTrigger); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Notifications_ListWithDefaultParametersWhenLessThanDefaultPageSize_ReturnsAllNotifications() { Assert.IsFalse(SetupFailed, "Setup did not succeed"); @@ -255,7 +255,7 @@ public IEnumerator Notifications_ListWithDefaultParametersWhenLessThanDefaultPag } } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Notifications_ListNotificationsWithPaginationParameters_ReturnsCorrectPage() { // Given @@ -329,7 +329,7 @@ public IEnumerator Notifications_ListNotificationsWithPaginationParameters_Retur } } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Notifications_MarkAllNotificationsAsRead_AllNotificationsMarkedAsRead() { // Given @@ -380,7 +380,7 @@ public IEnumerator Notifications_MarkAllNotificationsAsRead_AllNotificationsMark } } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Notifications_MarkSpecificNotificationsAsRead_SpecifiedNotificationsMarkedAsRead() { Assert.IsFalse(SetupFailed, "Setup did not succeed"); @@ -441,7 +441,7 @@ public IEnumerator Notifications_MarkSpecificNotificationsAsRead_SpecifiedNotifi Assert.AreEqual(listResponse.Notifications.Length - notificationIdsToMarkAsRead.Count(), listUnreadNotificationsAfterMarkAsReadResponse.Notifications.Length, "Not all notifications that were marked as read actually were"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Notifications_MarkSpecificNotificationsAsReadUsingConvenienceMethod_SpecifiedNotificationsMarkedAsRead() { Assert.IsFalse(SetupFailed, "Setup did not succeed"); @@ -497,7 +497,7 @@ public IEnumerator Notifications_MarkSpecificNotificationsAsReadUsingConvenience Assert.AreEqual(listResponse.Notifications.Length - 1, listUnreadNotificationsAfterMarkAsReadResponse.Notifications.Length, "Not all notifications that were marked as read actually were"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Notifications_MarkAllNotificationsAsReadUsingConvenienceMethod_SpecifiedNotificationsMarkedAsRead() { Assert.IsFalse(SetupFailed, "Setup did not succeed"); @@ -562,7 +562,9 @@ public IEnumerator Notifications_MarkAllNotificationsAsReadUsingConvenienceMetho Assert.AreEqual(CreatedTriggers.Count - notificationIdsToMarkAsRead.Length, listUnreadNotificationsAfterMarkAsReadResponse.Notifications.Length, "Not all notifications that were marked as read actually were"); } - [UnityTest] + + //TODO: Populate with new types + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Notifications_ConvenienceLookupTable_CanLookUpAllNotificationTypes() { Assert.IsFalse(SetupFailed, "Setup did not succeed"); @@ -638,7 +640,7 @@ public IEnumerator Notifications_ConvenienceLookupTable_CanLookUpAllNotification yield break; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Notifications_SendCustomNotificationsFromConsoleAndListNotifications_ListsAndParsesCustomNotificationsSuccessfully() { Assert.IsFalse(SetupFailed, "Setup did not succeed"); diff --git a/Tests/LootLockerTests/PlayMode/PingTest.cs b/Tests/LootLockerTests/PlayMode/PingTest.cs index e8c57c3c..aa97e024 100644 --- a/Tests/LootLockerTests/PlayMode/PingTest.cs +++ b/Tests/LootLockerTests/PlayMode/PingTest.cs @@ -1,10 +1,12 @@ using System.Collections; using LootLocker; using LootLocker.Requests; -using UnityEngine.TestTools; using LootLockerTestConfigurationUtils; -using UnityEngine; using Assert = UnityEngine.Assertions.Assert; +using System.ComponentModel; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; namespace LootLockerTests.PlayMode { @@ -94,7 +96,7 @@ public IEnumerator TearDown() Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } - [UnityTest] + [UnityTest, NUnit.Framework.Category("LootLocker"), NUnit.Framework.Category("LootLockerCI"), NUnit.Framework.Category("LootLockerCIFast")] public IEnumerator Ping_WithSession_Succeeds() { // Setup Succeeded @@ -123,7 +125,7 @@ public IEnumerator Ping_WithSession_Succeeds() Assert.IsNotNull(pingResponse.date, "Ping response contained no date"); } - [UnityTest] + [UnityTest, NUnit.Framework.Category("LootLocker"), NUnit.Framework.Category("LootLockerCI")] public IEnumerator Ping_WithoutSession_Fails() { // Setup Succeeded diff --git a/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs b/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs index f417dc42..38b16763 100644 --- a/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs +++ b/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs @@ -112,7 +112,7 @@ public IEnumerator TearDown() } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator PlayerFiles_UploadSimplePublicFile_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); diff --git a/Tests/LootLockerTests/PlayMode/PlayerInfoTest.cs b/Tests/LootLockerTests/PlayMode/PlayerInfoTest.cs index a8ad9c4d..41691e94 100644 --- a/Tests/LootLockerTests/PlayMode/PlayerInfoTest.cs +++ b/Tests/LootLockerTests/PlayMode/PlayerInfoTest.cs @@ -99,7 +99,7 @@ public IEnumerator TearDown() Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator PlayerInfo_GetCurrentPlayerInfo_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -155,7 +155,7 @@ public IEnumerator PlayerInfo_GetCurrentPlayerInfo_Succeeds() Assert.AreEqual(expectedPlayerLegacyId, actualPlayerLegacyId, "Player legacy id was not the same between session start and player info fetch"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator PlayerInfo_ListPlayerInfoForMultiplePlayersUsingDifferentIds_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); diff --git a/Tests/LootLockerTests/PlayMode/PlayerStorageTest.cs b/Tests/LootLockerTests/PlayMode/PlayerStorageTest.cs index cb5bab32..26bbfd3e 100644 --- a/Tests/LootLockerTests/PlayMode/PlayerStorageTest.cs +++ b/Tests/LootLockerTests/PlayMode/PlayerStorageTest.cs @@ -129,7 +129,7 @@ public LootLockerGetPersistentStorageRequest GetStorageTemplate() return data; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator PlayerStorage_CreatePayload_SucceedsAndReturnsStorage() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -165,7 +165,7 @@ public IEnumerator PlayerStorage_CreatePayload_SucceedsAndReturnsStorage() Assert.AreEqual(actualRequest.payload.Count, matchedKVPairs, "Not all storage kv pairs were matched in the response"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator PlayerStorage_UpdatePayload_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -241,7 +241,7 @@ public IEnumerator PlayerStorage_UpdatePayload_Succeeds() Assert.AreEqual(actualRequest.payload.Count, keysMatched, "Not all keys were found in fetched payload"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator PlayerStorage_GetOtherPlayersStorage_GetsOnlyPublicValues() { Assert.IsFalse(SetupFailed, "Failed to setup game"); diff --git a/Tests/LootLockerTests/PlayMode/RateLimiterTests.cs b/Tests/LootLockerTests/PlayMode/RateLimiterTests.cs index 8207d4dc..3acd9894 100644 --- a/Tests/LootLockerTests/PlayMode/RateLimiterTests.cs +++ b/Tests/LootLockerTests/PlayMode/RateLimiterTests.cs @@ -149,7 +149,7 @@ public IEnumerator UnityTearDown() yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_NormalAmountOfAverageRequests_DoesNotHitRateLimit() { // Given @@ -173,7 +173,7 @@ public IEnumerator RateLimiter_NormalAmountOfAverageRequests_DoesNotHitRateLimit yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_UndulatingLowLevelOfRequests_DoesNotHitRateLimit() { // Given @@ -202,7 +202,7 @@ public IEnumerator RateLimiter_UndulatingLowLevelOfRequests_DoesNotHitRateLimit( yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_FrequentSmallBursts_DoesNotHitRateLimit() { // Given @@ -238,7 +238,7 @@ public IEnumerator RateLimiter_FrequentSmallBursts_DoesNotHitRateLimit() yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_InfrequentLargeBursts_DoesNotHitRateLimit() { // Given @@ -278,7 +278,7 @@ public IEnumerator RateLimiter_InfrequentLargeBursts_DoesNotHitRateLimit() yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_ExcessiveQuickSuccessionRequests_HitsTripwireRateLimit() { // Given @@ -310,7 +310,7 @@ public IEnumerator RateLimiter_ExcessiveQuickSuccessionRequests_HitsTripwireRate yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_LowLevelBackgroundRequestsWithIntermittentBursts_HitsRateLimit() { // Given @@ -352,7 +352,7 @@ public IEnumerator RateLimiter_LowLevelBackgroundRequestsWithIntermittentBursts_ yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_SuddenHugeBurstBelowLimit_DoesNotTriggerRateLimit() { // Given @@ -390,7 +390,7 @@ public IEnumerator RateLimiter_SuddenHugeBurstBelowLimit_DoesNotTriggerRateLimit yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_SuddenHugeBurstAbove_LimitTriggersRateLimit() { // Given @@ -428,7 +428,7 @@ public IEnumerator RateLimiter_SuddenHugeBurstAbove_LimitTriggersRateLimit() yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_SuddenHugeBurstBelowLimitFollowedByAFewRequests_TriggersRateLimit() { // Given @@ -479,7 +479,7 @@ public IEnumerator RateLimiter_SuddenHugeBurstBelowLimitFollowedByAFewRequests_T yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_ConstantRequestsBelowTripWire_HitsMovingAverageRateLimit() { // Given @@ -505,7 +505,7 @@ public IEnumerator RateLimiter_ConstantRequestsBelowTripWire_HitsMovingAverageRa yield return null; } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RateLimiter_RateLimiterHit_ResetsAfter3Minutes() { // Given diff --git a/Tests/LootLockerTests/PlayMode/SessionRefreshTest.cs b/Tests/LootLockerTests/PlayMode/SessionRefreshTest.cs index 9b6338d9..2610d0c9 100644 --- a/Tests/LootLockerTests/PlayMode/SessionRefreshTest.cs +++ b/Tests/LootLockerTests/PlayMode/SessionRefreshTest.cs @@ -127,7 +127,7 @@ public string GetRandomName() LootLockerTestConfigurationUtilities.GetRandomVerb(); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator RefreshSession_ExpiredWhiteLabelSessionAndAutoRefreshEnabled_SessionIsAutomaticallyRefreshed() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -160,7 +160,7 @@ public IEnumerator RefreshSession_ExpiredWhiteLabelSessionAndAutoRefreshEnabled_ Assert.AreNotEqual(invalidToken, LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(null)?.SessionToken, "Token was not refreshed"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator RefreshSession_ExpiredWhiteLabelSessionButAutoRefreshDisabled_SessionDoesNotRefresh() { Assert.IsFalse(SetupFailed, "Failed to setup game"); diff --git a/Tests/LootLockerTests/PlayMode/SubmitScoreTest.cs b/Tests/LootLockerTests/PlayMode/SubmitScoreTest.cs index 54ee50c0..40994c9a 100644 --- a/Tests/LootLockerTests/PlayMode/SubmitScoreTest.cs +++ b/Tests/LootLockerTests/PlayMode/SubmitScoreTest.cs @@ -156,7 +156,7 @@ public IEnumerator TearDown() Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator SubmitScore_SubmitToPlayerLeaderboard_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -179,7 +179,7 @@ public IEnumerator SubmitScore_SubmitToPlayerLeaderboard_Succeeds() Assert.AreEqual(submittedScore, actualResponse.score, "Score was not as submitted"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator SubmitScore_SubmitToGenericLeaderboard_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -204,7 +204,7 @@ public IEnumerator SubmitScore_SubmitToGenericLeaderboard_Succeeds() Assert.AreEqual(submittedScore, actualResponse.score, "Score was not as submitted"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator SubmitScore_AttemptSubmitOnOverwriteScore_DoesNotUpdateScoreWhenScoreIsLower() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -237,7 +237,7 @@ public IEnumerator SubmitScore_AttemptSubmitOnOverwriteScore_DoesNotUpdateScoreW Assert.AreEqual(actualResponse.score, secondResponse.score, "Score got updated, even though it was smaller"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator SubmitScore_AttemptSubmitOnOverwriteScore_UpdatesScoreWhenScoreIsHigher() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -270,7 +270,7 @@ public IEnumerator SubmitScore_AttemptSubmitOnOverwriteScore_UpdatesScoreWhenSco Assert.AreEqual(actualResponse.score + 1, secondResponse.score, "Score did not get updated, even though it was higher"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator SubmitScore_SubmitOnOverwriteScoreWhenOverwriteIsAllowed_UpdatesScore() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -326,7 +326,7 @@ public IEnumerator SubmitScore_SubmitOnOverwriteScoreWhenOverwriteIsAllowed_Upda Assert.IsTrue(secondResponse.success, "SubmitScore failed"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator SubmitScore_SubmitToLeaderboardWithMetadata_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -350,7 +350,7 @@ public IEnumerator SubmitScore_SubmitToLeaderboardWithMetadata_Succeeds() Assert.AreEqual(submittedMetadata, actualResponse.metadata, "Metadata was not as expected"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator SubmitScore_SubmitMetadataToLeaderboardWithoutMetadata_IgnoresMetadata() { Assert.IsFalse(SetupFailed, "Failed to setup game"); diff --git a/Tests/LootLockerTests/PlayMode/TriggerTests.cs b/Tests/LootLockerTests/PlayMode/TriggerTests.cs index 58d3c545..71896fc4 100644 --- a/Tests/LootLockerTests/PlayMode/TriggerTests.cs +++ b/Tests/LootLockerTests/PlayMode/TriggerTests.cs @@ -185,7 +185,7 @@ private IEnumerator CreateTriggerWithReward(string triggerKey, string triggerNam onComplete?.Invoke(triggerCreated, errorMessage, createdTrigger); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Triggers_InvokeTriggerWithoutLimit_Succeeds() { // Setup Succeeded @@ -222,7 +222,7 @@ public IEnumerator Triggers_InvokeTriggerWithoutLimit_Succeeds() "The right key was not successfully executed"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Triggers_InvokeTriggerUnderLimit_Succeeds() { // Setup Succeeded @@ -258,7 +258,7 @@ public IEnumerator Triggers_InvokeTriggerUnderLimit_Succeeds() "The right key was not successfully executed"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Triggers_MultipleTriggersWithoutLimitCalledInSameCall_Succeeds() { // Setup Succeeded @@ -303,7 +303,7 @@ public IEnumerator Triggers_MultipleTriggersWithoutLimitCalledInSameCall_Succeed Assert.AreEqual(0, invokeResponse.Failed_keys?.Length, "Failed Keys were not of expected length"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Triggers_InvokeNonExistentTrigger_Fails() { // Setup Succeeded @@ -329,7 +329,7 @@ public IEnumerator Triggers_InvokeNonExistentTrigger_Fails() Assert.AreEqual(nonExistentTriggerKey, invokeResponse?.Failed_keys[0].Key, "Failed key was not as expected"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Triggers_InvokeTriggerOverLimit_Fails() { // Setup Succeeded @@ -373,7 +373,7 @@ public IEnumerator Triggers_InvokeTriggerOverLimit_Fails() Assert.AreEqual(createdTrigger.key, secondInvokeResponse?.Failed_keys[0].Key, "Failed key was not as expected"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Triggers_InvokeSameTriggerTwiceInSameCall_InvokesOnlyOnce() { // Setup Succeeded diff --git a/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs b/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs index 2addac56..2c84a969 100644 --- a/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs +++ b/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs @@ -98,7 +98,7 @@ public string GetRandomName() } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator WhiteLabel_SignUp_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -122,7 +122,7 @@ public IEnumerator WhiteLabel_SignUp_Succeeds() Assert.IsNotEmpty(actualResponse.CreatedAt, "Created At date is empty in the response"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator WhiteLabel_SignUpAndLogin_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -159,7 +159,7 @@ public IEnumerator WhiteLabel_SignUpAndLogin_Succeeds() Assert.IsNotEmpty(actualResponse.LoginResponse.SessionToken, "No session token found from login"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator WhiteLabel_SignUpAndLoginWithWrongPassword_Fails() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -200,7 +200,7 @@ public IEnumerator WhiteLabel_SignUpAndLoginWithWrongPassword_Fails() Assert.IsFalse(actualResponse.success, "Started White Label Session with wrong password"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator WhiteLabel_VerifySession_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -249,7 +249,7 @@ public IEnumerator WhiteLabel_VerifySession_Succeeds() Assert.IsTrue(actualResponse, "Could not Verify Session"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator WhiteLabel_RequestsAfterGameResetWhenWLDefaultUser_ReusesSession() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -301,7 +301,7 @@ public IEnumerator WhiteLabel_RequestsAfterGameResetWhenWLDefaultUser_ReusesSess Assert.AreEqual(expectedPlayerUlid, pingResponse.requestContext.player_ulid, "WL user not used for request"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator WhiteLabel_WLSessionStartByEmailAfterGameReset_ReusesSession() { Assert.IsFalse(SetupFailed, "Failed to setup game"); diff --git a/Tests/LootLockerTests/PlayMode/leaderboardDetailsTest.cs b/Tests/LootLockerTests/PlayMode/leaderboardDetailsTest.cs index bcb5c04d..875bca5d 100644 --- a/Tests/LootLockerTests/PlayMode/leaderboardDetailsTest.cs +++ b/Tests/LootLockerTests/PlayMode/leaderboardDetailsTest.cs @@ -136,7 +136,7 @@ public IEnumerator TearDown() Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Leaderboard_ListSchedule_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -171,7 +171,7 @@ public IEnumerator Leaderboard_ListSchedule_Succeeds() Assert.AreEqual(expectedCronExpression, actualResponse.schedule.cron_expression, "The submitted cron expression was not set on the leaderboard"); } - [UnityTest] + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] public IEnumerator Leaderboard_ListRewards_Succeeds() { Assert.IsFalse(SetupFailed, "Failed to setup game"); From ced0f08fc543865996eb6fa15697b06a89673b3e Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 22 Aug 2025 15:13:42 +0200 Subject: [PATCH 06/22] fix: Fixes after pull request --- .github/workflows/run-tests-and-package.yml | 7 +++++-- .../LootLockerTestConfigurationMetadata.cs | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests-and-package.yml b/.github/workflows/run-tests-and-package.yml index e871bd5c..9c5f7270 100644 --- a/.github/workflows/run-tests-and-package.yml +++ b/.github/workflows/run-tests-and-package.yml @@ -423,8 +423,10 @@ jobs: if [[ ${{ github.event_name == 'workflow_dispatch' }} == true ]]; then echo "LOOTLOCKER_URL=${{ INPUTS.LL_URL }}" | sed -e 's/https:\/\///g' >> $GITHUB_ENV; elif [ ${{ vars.LL_USE_STAGE }} == 'true' ]; then echo "LOOTLOCKER_URL=${{ SECRETS.LOOTLOCKER_API_STAGE_URL }}" | sed -e 's/https:\/\///g' >> $GITHUB_ENV; elif [ ${{ vars.LL_USE_LOCAL_BACKEND }} == 'true' ]; then echo "LOOTLOCKER_URL=localhost:8080" >> $GITHUB_ENV; else echo "LOOTLOCKER_URL=${{ SECRETS.LOOTLOCKER_API_PRODUCTION_URL }}" | sed -e 's/https:\/\///g' >> $GITHUB_ENV; fi if [[ ${{ github.event_name == 'workflow_dispatch' }} == true ]]; then echo "TARGET_ENVIRONMENT=CUSTOM" >> $GITHUB_ENV; echo "USE_TAILSCALE=true" >> $GITHUB_ENV; elif [ ${{ vars.LL_USE_STAGE }} == 'true' ]; then echo "TARGET_ENVIRONMENT=STAGE" >> $GITHUB_ENV; echo "USE_TAILSCALE=true" >> $GITHUB_ENV; elif [ ${{ vars.LL_USE_LOCAL_BACKEND }} == 'true' ]; then echo "TARGET_ENVIRONMENT=LOCAL" >> $GITHUB_ENV; echo "USE_TAILSCALE=false" >> $GITHUB_ENV; else echo "TARGET_ENVIRONMENT=PRODUCTION" >> $GITHUB_ENV; echo "USE_TAILSCALE=false" >> $GITHUB_ENV; fi COINFLIP=$(($RANDOM%${{ vars.LL_USE_LEGACY_HTTP_ONE_IN }})) + echo "Coinflip resulted in $COINFLIP" if [[ $COINFLIP -lt 1 ]]; then echo "USE_HTTP_EXECUTION_QUEUE=false" >> $GITHUB_ENV; else echo "USE_HTTP_EXECUTION_QUEUE=true" >> $GITHUB_ENV; fi COINFLIP=$(($RANDOM%2)) + echo "Coinflip resulted in $COINFLIP" if [[ $COINFLIP -lt 1 ]]; then echo "JSON_LIBRARY=newtonsoft" >> $GITHUB_ENV; else echo "JSON_LIBRARY=zerodep" >> $GITHUB_ENV; fi - name: Checkout this repository uses: actions/checkout@v4 @@ -489,11 +491,11 @@ jobs: - name: Set test category to full ci when PR to main if: ${{ github.event_name == 'pull_request' && github.base_ref == 'main' }} run: | - echo "TEST_CATEGORY=-testcategory LootLockerCI" >> $GITHUB_ENV + echo 'TEST_CATEGORY=-testCategory "LootLockerCI"' >> $GITHUB_ENV - name: Set test category to minimal ci if: ${{ github.event_name != 'pull_request' || github.base_ref != 'main' }} run: | - echo "TEST_CATEGORY=-testcategory LootLockerCIFast" >> $GITHUB_ENV + echo 'TEST_CATEGORY=-testCategory "LootLockerCIFast"' >> $GITHUB_ENV ####### RUN TESTS ########### - name: Cache Libraries if: ${{ vars.ENABLE_INTEGRATION_TESTS == 'true' }} @@ -606,6 +608,7 @@ jobs: validate-sdk: name: Validate SDK runs-on: [ubuntu-latest] + if: ${{ true == false }} needs: [editor-smoke-test] timeout-minutes: 8 env: diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs index 3618917c..4f0512dd 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs @@ -3,6 +3,10 @@ using System.Collections.Generic; using LootLocker; +#if LOOTLOCKER_USE_NEWTONSOFTJSON +using Newtonsoft.Json.Linq; +#endif + namespace LootLockerTestConfigurationUtils { public class LootLockerTestMetadata From 8cc1725decb07b58e5822e5b868ddfa8eea0d4e2 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 29 Aug 2025 09:23:03 +0200 Subject: [PATCH 07/22] feat: Add support for Friends and Followers --- Runtime/Client/LootLockerEndPoints.cs | 9 +- Runtime/Game/LootLockerSDKManager.cs | 126 ++++++++- Runtime/Game/Requests/FollowerRequests.cs | 81 ++++++ .../Game/Requests/FollowerRequests.cs.meta | 2 + Runtime/Game/Requests/FriendsRequest.cs | 14 +- .../PlayMode/FollowersTests.cs | 245 ++++++++++++++++++ .../PlayMode/FollowersTests.cs.meta | 2 + .../LootLockerTests/PlayMode/FriendsTests.cs | 2 - 8 files changed, 462 insertions(+), 19 deletions(-) create mode 100644 Runtime/Game/Requests/FollowerRequests.cs create mode 100644 Runtime/Game/Requests/FollowerRequests.cs.meta create mode 100644 Tests/LootLockerTests/PlayMode/FollowersTests.cs create mode 100644 Tests/LootLockerTests/PlayMode/FollowersTests.cs.meta diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index bb0cdf39..592cdd04 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -288,7 +288,6 @@ public class LootLockerEndPoints // Friends [Header("Friends")] -#if LOOTLOCKER_BETA_FRIENDS public static EndPointClass listFriends = new EndPointClass("player/friends", LootLockerHTTPMethod.GET); public static EndPointClass listIncomingFriendReqeusts = new EndPointClass("player/friends/incoming", LootLockerHTTPMethod.GET); public static EndPointClass listOutgoingFriendRequests = new EndPointClass("player/friends/outgoing", LootLockerHTTPMethod.GET); @@ -300,7 +299,13 @@ public class LootLockerEndPoints public static EndPointClass blockPlayer = new EndPointClass("player/friends/{0}/block", LootLockerHTTPMethod.POST); public static EndPointClass unblockPlayer = new EndPointClass("player/friends/{0}/unblock", LootLockerHTTPMethod.POST); public static EndPointClass deleteFriend = new EndPointClass("player/friends/{0}", LootLockerHTTPMethod.DELETE); -#endif + + // Followers + [Header("Followers")] + public static EndPointClass listFollowers = new EndPointClass("player/{0}/followers", LootLockerHTTPMethod.GET); + public static EndPointClass listFollowing = new EndPointClass("player/{0}/following", LootLockerHTTPMethod.GET); + public static EndPointClass followPlayer = new EndPointClass("player/{0}/followers/follow", LootLockerHTTPMethod.POST); + public static EndPointClass unfollowPlayer = new EndPointClass("player/{0}/followers/unfollow", LootLockerHTTPMethod.DELETE); // Entitlements [Header("Entitlements")] diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 8dfbcc77..c41ad682 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6743,7 +6743,6 @@ private static void SendFeedback(LootLockerFeedbackTypes type, string ulid, stri #endregion #region Friends -#if LOOTLOCKER_BETA_FRIENDS /// /// List friends for the currently logged in player /// @@ -6975,7 +6974,130 @@ public static void DeleteFriend(string playerID, Action { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } -#endif + #endregion + + #region Followers + /// + /// List followers of the currently logged in player + /// + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListFollowers(Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(forPlayerWithUlid); + ListFollowers(playerData.PublicUID, onComplete, forPlayerWithUlid); + } + + /// + /// List followers that the specified player has + /// + /// The public UID of the player whose followers to list + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListFollowers(string playerPublicUID, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + + var formattedEndPoint = LootLockerEndPoints.listFollowers.WithPathParameter(playerPublicUID); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, formattedEndPoint, LootLockerEndPoints.listFollowers.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// List what players the currently logged in player is following + /// + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListFollowing(Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(forPlayerWithUlid); + ListFollowing(playerData.PublicUID, onComplete, forPlayerWithUlid); + } + + /// + /// List players that the specified player is following + /// + /// The public UID of the player for which to list following players + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListFollowing(string playerPublicUID, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + + var formattedEndPoint = LootLockerEndPoints.listFollowing.WithPathParameter(playerPublicUID); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, formattedEndPoint, LootLockerEndPoints.listFollowing.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// Follow the specified player + /// + /// The public uid of the player to follow + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void FollowPlayer(string playerPublicUID, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + if (string.IsNullOrEmpty(playerPublicUID)) + { + onComplete?.Invoke(LootLockerResponseFactory.ClientError("A player public UID needs to be provided for this method", forPlayerWithUlid)); + } + + var formattedEndPoint = LootLockerEndPoints.followPlayer.WithPathParameter(playerPublicUID); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, formattedEndPoint, LootLockerEndPoints.followPlayer.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// Unfollow the specified player + /// + /// The public uid of the player to unfollow + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void UnfollowPlayer(string playerPublicUID, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + if (string.IsNullOrEmpty(playerPublicUID)) + { + onComplete?.Invoke(LootLockerResponseFactory.ClientError("A player public UID needs to be provided for this method", forPlayerWithUlid)); + } + + var formattedEndPoint = LootLockerEndPoints.unfollowPlayer.WithPathParameter(playerPublicUID); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, formattedEndPoint, LootLockerEndPoints.unfollowPlayer.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } #endregion #region Currency diff --git a/Runtime/Game/Requests/FollowerRequests.cs b/Runtime/Game/Requests/FollowerRequests.cs new file mode 100644 index 00000000..4fea5b47 --- /dev/null +++ b/Runtime/Game/Requests/FollowerRequests.cs @@ -0,0 +1,81 @@ +using System; +#if LOOTLOCKER_USE_NEWTONSOFTJSON +using Newtonsoft.Json; +#else +using LLlibs.ZeroDepJson; +#endif + +namespace LootLocker.Requests +{ + //================================================== + // Data Definitions + //================================================== + + /// + /// + public class LootLockerFollower + { + /// + /// The id of the player + /// + public string player_id { get; set; } + /// + /// The name (if any has been set) of the player + /// + public string player_name { get; set; } + /// + /// The public uid of the player + /// + public string publicuid { get; set; } + /// + /// When the player's account was created + /// + public DateTime created_at { get; set; } + } + + //================================================== + // Response Definitions + //================================================== + + /// + /// + public class LootLockerListFollowersResponse : LootLockerResponse + { + /// + /// A list of the followers for the specified player + /// + public LootLockerFollower[] followers { get; set; } + /// + /// Pagination data for the request + /// + public LootLockerPaginationResponse pagination { get; set; } + } + + /// + /// + public class LootLockerListFollowingResponse : LootLockerResponse + { + /// + /// A list of the players that the specified player is following + /// +#if LOOTLOCKER_USE_NEWTONSOFTJSON + [JsonProperty("followers")] +#else + [Json(Name = "followers")] +#endif + + public LootLockerFollower[] following { get; set; } + /// + /// Pagination data for the request + /// + public LootLockerPaginationResponse pagination { get; set; } + } + + /// + /// + public class LootLockerFollowersOperationResponse : LootLockerResponse + { + // Empty unless errors occured + } + +} \ No newline at end of file diff --git a/Runtime/Game/Requests/FollowerRequests.cs.meta b/Runtime/Game/Requests/FollowerRequests.cs.meta new file mode 100644 index 00000000..022ca0ca --- /dev/null +++ b/Runtime/Game/Requests/FollowerRequests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: eae6d619bbe818847b05830417f0a8e4 \ No newline at end of file diff --git a/Runtime/Game/Requests/FriendsRequest.cs b/Runtime/Game/Requests/FriendsRequest.cs index 7d4d9a83..4a728c6f 100644 --- a/Runtime/Game/Requests/FriendsRequest.cs +++ b/Runtime/Game/Requests/FriendsRequest.cs @@ -1,14 +1,5 @@ using System; -#if LOOTLOCKER_BETA_FRIENDS - -//================================================== -// Enum Definitions -//================================================== -namespace LootLocker.LootLockerEnums -{ -} - namespace LootLocker.Requests { //================================================== @@ -123,7 +114,4 @@ public class LootLockerFriendsOperationResponse : LootLockerResponse { // Empty unless errors occured } - -} - -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/Tests/LootLockerTests/PlayMode/FollowersTests.cs b/Tests/LootLockerTests/PlayMode/FollowersTests.cs new file mode 100644 index 00000000..7db0e819 --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/FollowersTests.cs @@ -0,0 +1,245 @@ +using System.Collections; +using LootLocker; +using LootLocker.Requests; +using LootLockerTestConfigurationUtils; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace LootLockerTests.PlayMode +{ + public class FollowersTests + { + + private LootLockerTestGame gameUnderTest = null; + private LootLockerConfig configCopy = null; + private static int TestCounter = 0; + private bool SetupFailed = false; + + [UnitySetUp] + public IEnumerator Setup() + { + TestCounter++; + configCopy = LootLockerConfig.current; + Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} setup #####"); + + if (!LootLockerConfig.ClearSettings()) + { + Debug.LogError("Could not clear LootLocker config"); + } + + // Create game + bool gameCreationCallCompleted = false; + LootLockerTestGame.CreateGame(testName: this.GetType().Name + TestCounter + " ", onComplete: (success, errorMessage, game) => + { + if (!success) + { + gameCreationCallCompleted = true; + Debug.LogError(errorMessage); + SetupFailed = true; + } + gameUnderTest = game; + gameCreationCallCompleted = true; + }); + yield return new WaitUntil(() => gameCreationCallCompleted); + if (SetupFailed) + { + yield break; + } + gameUnderTest?.SwitchToStageEnvironment(); + + // Enable guest platform + bool enableGuestLoginCallCompleted = false; + gameUnderTest?.EnableGuestLogin((success, errorMessage) => + { + if (!success) + { + Debug.LogError(errorMessage); + SetupFailed = true; + } + enableGuestLoginCallCompleted = true; + }); + yield return new WaitUntil(() => enableGuestLoginCallCompleted); + if (SetupFailed) + { + yield break; + } + Assert.IsTrue(gameUnderTest?.InitializeLootLockerSDK(), "Successfully created test game and initialized LootLocker"); + + Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####"); + } + + [UnityTearDown] + public IEnumerator TearDown() + { + Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} test case #####"); + if (gameUnderTest != null) + { + bool gameDeletionCallCompleted = false; + gameUnderTest.DeleteGame(((success, errorMessage) => + { + if (!success) + { + Debug.LogError(errorMessage); + } + + gameUnderTest = null; + gameDeletionCallCompleted = true; + })); + yield return new WaitUntil(() => gameDeletionCallCompleted); + } + + LootLockerStateData.ClearAllSavedStates(); + + LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, + configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public IEnumerator Followers_FollowPlayer_AddsToFollowingListAndFollowersList() + { + // Given + string Player1Identifier = "Follower-1"; + string Player1PublicUid = ""; + string Player1Ulid = ""; + string Player2Identifier = "Followee-2"; + string Player2PublicUid = ""; + string Player2Ulid = ""; + + bool signInCompleted = false; + LootLockerSDKManager.StartGuestSession(Player1Identifier, response => + { + Player1PublicUid = response?.public_uid; + Player1Ulid = response?.player_ulid; + signInCompleted = true; + }); + yield return new WaitUntil(() => signInCompleted); + Assert.IsNotEmpty(Player1PublicUid, "Guest Session 1 failed"); + + signInCompleted = false; + LootLockerSDKManager.StartGuestSession(Player2Identifier, response => + { + Player2PublicUid = response?.public_uid; + Player2Ulid = response?.player_ulid; + signInCompleted = true; + }); + yield return new WaitUntil(() => signInCompleted); + Assert.IsNotEmpty(Player2PublicUid, "Guest Session 2 failed"); + + // When + bool followCompleted = false; + LootLockerFollowersOperationResponse followResponse = null; + LootLockerSDKManager.FollowPlayer(Player2PublicUid, response => + { + followResponse = response; + followCompleted = true; + }, Player1Ulid); + yield return new WaitUntil(() => followCompleted); + Assert.IsTrue(followResponse.success, "Follow request failed"); + + // Then + bool listFollowingCompleted = false; + LootLockerListFollowingResponse followingResponse = null; + LootLockerSDKManager.ListFollowing(response => + { + followingResponse = response; + listFollowingCompleted = true; + }, Player1Ulid); + yield return new WaitUntil(() => listFollowingCompleted); + Assert.IsTrue(followingResponse.success, "List following failed"); + + bool listFollowersCompleted = false; + LootLockerListFollowersResponse followersResponse = null; + LootLockerSDKManager.ListFollowers(response => + { + followersResponse = response; + listFollowersCompleted = true; + }, Player2Ulid); + yield return new WaitUntil(() => listFollowersCompleted); + Assert.IsTrue(followersResponse.success, "List followers failed"); + + Assert.AreEqual(1, followingResponse?.following?.Length, "Following list count incorrect"); + Assert.AreEqual(1, followersResponse?.followers?.Length, "Followers list count incorrect"); + Assert.AreEqual(Player2Ulid, followingResponse?.following?[0]?.player_id, "Player 1 not following player 2"); + Assert.AreEqual(Player1Ulid, followersResponse?.followers?[0]?.player_id, "Player 2 not followed by player 1"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public IEnumerator Followers_UnfollowPlayer_RemovesFromFollowingListAndFollowersList() + { + // Given + string Player1Identifier = "Follower-1"; + string Player1PublicUid = ""; + string Player1Ulid = ""; + string Player2Identifier = "Followee-2"; + string Player2PublicUid = ""; + string Player2Ulid = ""; + + bool signInCompleted = false; + LootLockerSDKManager.StartGuestSession(Player1Identifier, response => + { + Player1PublicUid = response?.public_uid; + Player1Ulid = response?.player_ulid; + signInCompleted = true; + }); + yield return new WaitUntil(() => signInCompleted); + Assert.IsNotEmpty(Player1PublicUid, "Guest Session 1 failed"); + + signInCompleted = false; + LootLockerSDKManager.StartGuestSession(Player2Identifier, response => + { + Player2PublicUid = response?.public_uid; + Player2Ulid = response?.player_ulid; + signInCompleted = true; + }); + yield return new WaitUntil(() => signInCompleted); + Assert.IsNotEmpty(Player2PublicUid, "Guest Session 2 failed"); + + bool followCompleted = false; + LootLockerFollowersOperationResponse followResponse = null; + LootLockerSDKManager.FollowPlayer(Player2PublicUid, response => + { + followResponse = response; + followCompleted = true; + }, Player1Ulid); + yield return new WaitUntil(() => followCompleted); + Assert.IsTrue(followResponse.success, "Follow request failed"); + + // When + bool unfollowCompleted = false; + LootLockerFollowersOperationResponse unfollowResponse = null; + LootLockerSDKManager.UnfollowPlayer(Player2PublicUid, response => + { + unfollowResponse = response; + unfollowCompleted = true; + }, Player1Ulid); + yield return new WaitUntil(() => unfollowCompleted); + Assert.IsTrue(unfollowResponse.success, "Unfollow request failed"); + + // Then + bool listFollowingCompleted = false; + LootLockerListFollowingResponse followingResponse = null; + LootLockerSDKManager.ListFollowing(response => + { + followingResponse = response; + listFollowingCompleted = true; + }, Player1Ulid); + yield return new WaitUntil(() => listFollowingCompleted); + Assert.IsTrue(followingResponse.success, "List following failed"); + + bool listFollowersCompleted = false; + LootLockerListFollowersResponse followersResponse = null; + LootLockerSDKManager.ListFollowers(response => + { + followersResponse = response; + listFollowersCompleted = true; + }, Player2Ulid); + yield return new WaitUntil(() => listFollowersCompleted); + Assert.IsTrue(followersResponse.success, "List followers failed"); + + Assert.IsEmpty(followingResponse?.following, "Following list not empty after unfollow"); + Assert.IsEmpty(followersResponse?.followers, "Followers list not empty after unfollow"); + } + } +} \ No newline at end of file diff --git a/Tests/LootLockerTests/PlayMode/FollowersTests.cs.meta b/Tests/LootLockerTests/PlayMode/FollowersTests.cs.meta new file mode 100644 index 00000000..d3cdd663 --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/FollowersTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6e49120499416b841a776c7b8c916c47 \ No newline at end of file diff --git a/Tests/LootLockerTests/PlayMode/FriendsTests.cs b/Tests/LootLockerTests/PlayMode/FriendsTests.cs index 82d83aa3..b1cb9b40 100644 --- a/Tests/LootLockerTests/PlayMode/FriendsTests.cs +++ b/Tests/LootLockerTests/PlayMode/FriendsTests.cs @@ -11,7 +11,6 @@ namespace LootLockerTests.PlayMode public class FriendsTests { -#if LOOTLOCKER_BETA_FRIENDS private LootLockerTestGame gameUnderTest = null; private LootLockerConfig configCopy = null; private static int TestCounter = 0; @@ -819,6 +818,5 @@ public IEnumerator Friends_BlockPlayer_RemovesBlockedPlayerFromFriendsList() } Assert.IsFalse(foundFriendUlidPostBlockPlayer2, "Friend ulid was present in friends list pre block"); } -#endif } } \ No newline at end of file From 0e5539367fb72e6246c249ec3b145b9e635450c1 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Wed, 27 Aug 2025 11:43:15 +0200 Subject: [PATCH 08/22] feat: Add support for Epic IAP --- Runtime/Client/LootLockerEndPoints.cs | 1 + Runtime/Game/LootLockerSDKManager.cs | 54 ++++++++++++++++++++++++ Runtime/Game/Requests/PurchaseRequest.cs | 35 +++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 592cdd04..05e0c36e 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -208,6 +208,7 @@ public class LootLockerEndPoints public static EndPointClass purchaseCatalogItem = new EndPointClass("purchase", LootLockerHTTPMethod.POST); public static EndPointClass redeemAppleAppStorePurchase = new EndPointClass("store/apple/redeem", LootLockerHTTPMethod.POST); public static EndPointClass redeemGooglePlayStorePurchase = new EndPointClass("store/google/redeem", LootLockerHTTPMethod.POST); + public static EndPointClass redeemEpicStorePurchase = new EndPointClass("store/epic/redeem", LootLockerHTTPMethod.POST); public static EndPointClass beginSteamPurchaseRedemption = new EndPointClass("store/steam/redeem/begin", LootLockerHTTPMethod.POST); public static EndPointClass querySteamPurchaseRedemptionStatus = new EndPointClass("store/steam/redeem/query", LootLockerHTTPMethod.POST); diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index c41ad682..9ca427a3 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -5831,6 +5831,60 @@ public static void RedeemGooglePlayStorePurchaseForClass(string productId, strin LootLockerServerRequest.CallAPI(forPlayerWithUlid, LootLockerEndPoints.redeemGooglePlayStorePurchase.endPoint, LootLockerEndPoints.redeemGooglePlayStorePurchase.httpMethod, body, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } + /// + /// Redeem a purchase that was made successfully towards the Epic Store for the current player + /// + /// The Epic account id of the account that this purchase was made for + /// The token from Epic used to allow the LootLocker backend to verify ownership of the specified entitlements. This is sometimes referred to as the Access Token or the Auth Token + /// The ids of the purchased entitlements that you wish to redeem + /// onComplete Action for handling the response + /// Optional: Execute the request for the specified player. If not supplied, the default player will be used. + /// Optional: The sandbox id to use for the request, only applicable for "sandbox purchases" (ie, fake development purchases) + public static void RedeemEpicStorePurchaseForPlayer(string accountId, string bearerToken, List entitlementIds, Action onComplete, string forPlayerWithUlid = null, string sandboxId = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + var body = LootLockerJson.SerializeObject(new LootLockerRedeemEpicStorePurchaseForPlayerRequest() + { + account_id = accountId, + bearer_token = bearerToken, + entitlement_ids = entitlementIds, + sandbox_id = sandboxId + }); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, LootLockerEndPoints.redeemEpicStorePurchase.endPoint, LootLockerEndPoints.redeemEpicStorePurchase.httpMethod, body, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// Redeem a purchase that was made successfully towards the Epic Store for a class that the current player owns + /// + /// The Epic account id of the account that this purchase was made for + /// The token from Epic used to allow the LootLocker backend to verify ownership of the specified entitlements. This is sometimes referred to as the Access Token or the Auth Token + /// The ids of the purchased entitlements that you wish to redeem + /// The id of the class to redeem this purchase for + /// onComplete Action for handling the response + /// Optional: Execute the request for the specified player. If not supplied, the default player will be used. + /// Optional: The sandbox id to use for the request, only applicable for "sandbox purchases" (ie, fake development purchases) + public static void RedeemEpicStorePurchaseForClass(string accountId, string bearerToken, List entitlementIds, int classId, Action onComplete, string forPlayerWithUlid = null, string sandboxId = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + var body = LootLockerJson.SerializeObject(new LootLockerRedeemEpicStorePurchaseForClassRequest() + { + account_id = accountId, + bearer_token = bearerToken, + entitlement_ids = entitlementIds, + class_id = classId, + sandbox_id = sandboxId + }); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, LootLockerEndPoints.redeemEpicStorePurchase.endPoint, LootLockerEndPoints.redeemEpicStorePurchase.httpMethod, body, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + /// /// Begin a Steam purchase with the given settings that when finalized will redeem the specified catalog item /// diff --git a/Runtime/Game/Requests/PurchaseRequest.cs b/Runtime/Game/Requests/PurchaseRequest.cs index 56bf2cbb..777e2561 100644 --- a/Runtime/Game/Requests/PurchaseRequest.cs +++ b/Runtime/Game/Requests/PurchaseRequest.cs @@ -1,6 +1,8 @@ using LootLocker.Requests; using System; using LootLocker.LootLockerEnums; +using System.Collections.Generic; + #if LOOTLOCKER_USE_NEWTONSOFTJSON using Newtonsoft.Json; #else @@ -127,6 +129,39 @@ public class LootLockerRedeemGooglePlayStorePurchaseForClassRequest : LootLocker public int class_id { get; set; } } + public class LootLockerRedeemEpicStorePurchaseForPlayerRequest + { + /// + /// The epic account id of the account that this purchase was made for + /// + public string account_id; + /// + /// This is the token from epic used to allow the LootLocker backend to verify ownership of the specified entitlements. This is sometimes referred to as the Access Token or the Auth Token + /// + public string bearer_token; + /// + /// The ids of the purchased entitlements that you wish to redeem + /// + public List entitlement_ids; + /// + /// The sandbox id to use for the request, only applicable for "sandbox purchases" (ie, fake development purchases) + /// + public string sandbox_id; + } + + public class LootLockerRedeemEpicStorePurchaseForClassRequest : LootLockerRedeemEpicStorePurchaseForPlayerRequest + { + /// + /// The ulid of the character to redeem this purchase for + /// +#if LOOTLOCKER_USE_NEWTONSOFTJSON + [JsonProperty("character_id")] +#else + [Json(Name = "character_id")] +#endif + public int class_id; + } + /// /// /// From b6c3d4f7e3ea4c749cf4d52df3fb3be5859f21e0 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 29 Aug 2025 09:44:01 +0200 Subject: [PATCH 09/22] doc: Update documentation for epic iap after testing --- Runtime/Game/LootLockerSDKManager.cs | 12 ++++++------ Runtime/Game/Requests/PurchaseRequest.cs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 9ca427a3..cbfffd9f 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -5835,12 +5835,12 @@ public static void RedeemGooglePlayStorePurchaseForClass(string productId, strin /// Redeem a purchase that was made successfully towards the Epic Store for the current player /// /// The Epic account id of the account that this purchase was made for - /// The token from Epic used to allow the LootLocker backend to verify ownership of the specified entitlements. This is sometimes referred to as the Access Token or the Auth Token + /// This is the token from epic used to allow the LootLocker backend to verify ownership of the specified entitlements. This is sometimes referred to as the Server Auth Ticket or Auth Token depending on your Epic integration. /// The ids of the purchased entitlements that you wish to redeem + /// The Sandbox Id configured for the game making the purchase (this is the sandbox id from your epic online service configuration) /// onComplete Action for handling the response /// Optional: Execute the request for the specified player. If not supplied, the default player will be used. - /// Optional: The sandbox id to use for the request, only applicable for "sandbox purchases" (ie, fake development purchases) - public static void RedeemEpicStorePurchaseForPlayer(string accountId, string bearerToken, List entitlementIds, Action onComplete, string forPlayerWithUlid = null, string sandboxId = null) + public static void RedeemEpicStorePurchaseForPlayer(string accountId, string bearerToken, List entitlementIds, string sandboxId, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { @@ -5861,13 +5861,13 @@ public static void RedeemEpicStorePurchaseForPlayer(string accountId, string bea /// Redeem a purchase that was made successfully towards the Epic Store for a class that the current player owns /// /// The Epic account id of the account that this purchase was made for - /// The token from Epic used to allow the LootLocker backend to verify ownership of the specified entitlements. This is sometimes referred to as the Access Token or the Auth Token + /// This is the token from epic used to allow the LootLocker backend to verify ownership of the specified entitlements. This is sometimes referred to as the Server Auth Ticket or Auth Token depending on your Epic integration. /// The ids of the purchased entitlements that you wish to redeem + /// The Sandbox Id configured for the game making the purchase (this is the sandbox id from your epic online service configuration) /// The id of the class to redeem this purchase for /// onComplete Action for handling the response /// Optional: Execute the request for the specified player. If not supplied, the default player will be used. - /// Optional: The sandbox id to use for the request, only applicable for "sandbox purchases" (ie, fake development purchases) - public static void RedeemEpicStorePurchaseForClass(string accountId, string bearerToken, List entitlementIds, int classId, Action onComplete, string forPlayerWithUlid = null, string sandboxId = null) + public static void RedeemEpicStorePurchaseForClass(string accountId, string bearerToken, List entitlementIds, string sandboxId, int classId, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { diff --git a/Runtime/Game/Requests/PurchaseRequest.cs b/Runtime/Game/Requests/PurchaseRequest.cs index 777e2561..665097c5 100644 --- a/Runtime/Game/Requests/PurchaseRequest.cs +++ b/Runtime/Game/Requests/PurchaseRequest.cs @@ -136,7 +136,7 @@ public class LootLockerRedeemEpicStorePurchaseForPlayerRequest /// public string account_id; /// - /// This is the token from epic used to allow the LootLocker backend to verify ownership of the specified entitlements. This is sometimes referred to as the Access Token or the Auth Token + /// This is the token from epic used to allow the LootLocker backend to verify ownership of the specified entitlements. This is sometimes referred to as the Server Auth Ticket or Auth Token depending on your Epic integration. /// public string bearer_token; /// @@ -144,7 +144,7 @@ public class LootLockerRedeemEpicStorePurchaseForPlayerRequest /// public List entitlement_ids; /// - /// The sandbox id to use for the request, only applicable for "sandbox purchases" (ie, fake development purchases) + /// The Sandbox Id configured for the game making the purchase (this is the sandbox id from your epic online service configuration) /// public string sandbox_id; } From ed5babb3ee30c86fe546596d9b6ce1726011d296 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 5 Sep 2025 11:49:26 +0200 Subject: [PATCH 10/22] fix: Remove online status until it's working --- Runtime/Game/Requests/FollowerRequests.cs | 2 +- Runtime/Game/Requests/FriendsRequest.cs | 14 +++++++------- Tests/LootLockerTests/PlayMode/FriendsTests.cs | 15 ++++++++++++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Runtime/Game/Requests/FollowerRequests.cs b/Runtime/Game/Requests/FollowerRequests.cs index 4fea5b47..ed764631 100644 --- a/Runtime/Game/Requests/FollowerRequests.cs +++ b/Runtime/Game/Requests/FollowerRequests.cs @@ -30,7 +30,7 @@ public class LootLockerFollower /// /// When the player's account was created /// - public DateTime created_at { get; set; } + public DateTime createdat { get; set; } } //================================================== diff --git a/Runtime/Game/Requests/FriendsRequest.cs b/Runtime/Game/Requests/FriendsRequest.cs index 4a728c6f..ca21edaf 100644 --- a/Runtime/Game/Requests/FriendsRequest.cs +++ b/Runtime/Game/Requests/FriendsRequest.cs @@ -23,10 +23,6 @@ public class LootLockerFriend /// public string public_uid { get; set; } /// - /// When the friend request for this friend was accepted - /// - public DateTime accepted_at { get; set; } - /// /// When the player's account was created /// public DateTime created_at { get; set; } @@ -34,12 +30,16 @@ public class LootLockerFriend /// /// - public class LootLockerFriendWithOnlineStatus : LootLockerFriend + public class LootLockerAcceptedFriend : LootLockerFriend { /// /// Whether or not the player is currently online /// - public bool online { get; set; } + //public bool online { get; set; } + /// + /// When the friend request for this friend was accepted + /// + public DateTime accepted_at { get; set; } } /// @@ -75,7 +75,7 @@ public class LootLockerListFriendsResponse : LootLockerResponse /// /// A list of the friends for the currently logged in player /// - public LootLockerFriendWithOnlineStatus[] friends { get; set; } + public LootLockerAcceptedFriend[] friends { get; set; } } /// diff --git a/Tests/LootLockerTests/PlayMode/FriendsTests.cs b/Tests/LootLockerTests/PlayMode/FriendsTests.cs index b1cb9b40..c3af4145 100644 --- a/Tests/LootLockerTests/PlayMode/FriendsTests.cs +++ b/Tests/LootLockerTests/PlayMode/FriendsTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using LootLocker; using LootLocker.Requests; @@ -434,12 +435,20 @@ public IEnumerator Friends_AcceptIncomingFriendRequest_AddsToFriendsListAndRemov Assert.Greater(incomingFriendRequestsResponse.incoming?.Length, incomingFriendRequestsPostAcceptResponse.incoming?.Length, "Friend request was not removed when accepting"); Assert.Greater(outgoingFriendRequestsResponse.outgoing?.Length, outgoingFriendRequestsPostAcceptResponse.outgoing?.Length, "Friend request was not removed when accepting"); - bool foundFriendUlid = false; + LootLockerAcceptedFriend foundFriend = null; foreach (var player in listFriendsResponse?.friends) { - foundFriendUlid |= player.player_id.Equals(Player2Ulid, System.StringComparison.OrdinalIgnoreCase); + if (player.player_id.Equals(Player2Ulid, System.StringComparison.OrdinalIgnoreCase)) + { + foundFriend = player; + } } - Assert.IsTrue(foundFriendUlid, "Friend ulid was not present in friends list"); + Assert.IsNotNull(foundFriend, "Friend ulid was not present in friends list"); + Assert.IsNotEmpty(foundFriend.public_uid, "Friend public uid was not populated in friends list"); + Assert.IsNotNull(foundFriend.created_at, "Friend created at was not populated in friends list"); + Assert.AreNotEqual(foundFriend.created_at, default(DateTime), "Friend created at was not populated in friends list"); + Assert.IsNotNull(foundFriend.accepted_at, "Friend accepted at was not populated in friends list"); + Assert.AreNotEqual(foundFriend.accepted_at, default(DateTime), "Friend accepted at was not populated in friends list"); yield break; } From 12a97eb68207099149c3b34ec835bae1dba4bf55 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Mon, 8 Sep 2025 14:46:38 +0200 Subject: [PATCH 11/22] fix: Make session refreshes work with HTTP Execution Queue --- .github/workflows/run-tests-and-package.yml | 2 + Runtime/Client/LootLockerHTTPClient.cs | 107 ++++++++++---------- Runtime/Game/LootLockerLogger.cs | 11 ++ 3 files changed, 68 insertions(+), 52 deletions(-) diff --git a/.github/workflows/run-tests-and-package.yml b/.github/workflows/run-tests-and-package.yml index 9c5f7270..3667707c 100644 --- a/.github/workflows/run-tests-and-package.yml +++ b/.github/workflows/run-tests-and-package.yml @@ -12,6 +12,7 @@ on: branches: # Made towards the following - main - dev + - ci/* workflow_dispatch: inputs: LL_URL: @@ -23,6 +24,7 @@ jobs: name: Test SDK in Editor runs-on: [ubuntu-latest] needs: [] +# if: ( true == false ) timeout-minutes: 10 env: LL_USE_STAGE: false diff --git a/Runtime/Client/LootLockerHTTPClient.cs b/Runtime/Client/LootLockerHTTPClient.cs index ad468d5a..eff63466 100644 --- a/Runtime/Client/LootLockerHTTPClient.cs +++ b/Runtime/Client/LootLockerHTTPClient.cs @@ -216,7 +216,7 @@ public void OverrideCertificateHandler(CertificateHandler certificateHandler) private Dictionary HTTPExecutionQueue = new Dictionary(); private List CompletedRequestIDs = new List(); - private List ExecutionItemsNeedingRefresh = new List(); + private UniqueList ExecutionItemsNeedingRefresh = new UniqueList(); private List OngoingIdsToCleanUp = new List(); private void OnDestroy() @@ -232,7 +232,6 @@ private void OnDestroy() void Update() { - ExecutionItemsNeedingRefresh.Clear(); // Process the execution queue foreach (var executionItem in HTTPExecutionQueue.Values) { @@ -277,7 +276,7 @@ void Update() if (Result == HTTPExecutionQueueProcessingResult.NeedsSessionRefresh) { //Bulk handle session refreshes at the end - ExecutionItemsNeedingRefresh.Add(executionItem.RequestData.RequestId); + ExecutionItemsNeedingRefresh.AddUnique(executionItem.RequestData.RequestId); continue; } else if (Result == HTTPExecutionQueueProcessingResult.WaitForNextTick || Result == HTTPExecutionQueueProcessingResult.None) @@ -292,61 +291,28 @@ void Update() // Bulk session refresh requests if (ExecutionItemsNeedingRefresh.Count > 0) { - UniqueList refreshSessionsForPlayers = new UniqueList(); foreach (string executionItemId in ExecutionItemsNeedingRefresh) { if (HTTPExecutionQueue.TryGetValue(executionItemId, out var executionItem)) { - CurrentlyOngoingRequests.Remove(executionItem.RequestData.RequestId); - executionItem.IsWaitingForSessionRefresh = true; - executionItem.RequestData.TimesRetried++; - refreshSessionsForPlayers.AddUnique(executionItem.RequestData.ForPlayerWithUlid); - // Unsetting web request fields will make the execution queue retry it - executionItem.AbortRequest(); - } - } - - foreach (string refreshForPlayerUlid in refreshSessionsForPlayers) + if (executionItem == null) { - StartCoroutine(RefreshSession(refreshForPlayerUlid, newSessionResponse => - { - foreach (string executionItemId in ExecutionItemsNeedingRefresh) - { - if (HTTPExecutionQueue.TryGetValue(executionItemId, out var executionItem)) - { - if (!executionItem.RequestData.ForPlayerWithUlid.Equals(refreshForPlayerUlid)) - { - // This refresh callback was not for this user + ExecutionItemsNeedingRefresh.Remove(executionItemId); continue; } - var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(executionItem.RequestData.ForPlayerWithUlid); - string tokenBeforeRefresh = executionItem.RequestData.ExtraHeaders["x-session-token"]; - string tokenAfterRefresh = playerData?.SessionToken; - if (string.IsNullOrEmpty(tokenAfterRefresh) || tokenBeforeRefresh.Equals(playerData.SessionToken)) + else if (executionItem.IsWaitingForSessionRefresh) { - // Session refresh failed so abort call chain - CallListenersAndMarkDone(executionItem, LootLockerResponseFactory.TokenExpiredError(executionItem.RequestData.ForPlayerWithUlid)); + // Already waiting for session refresh continue; } - - // Session refresh worked so update the session token header - if (executionItem.RequestData.CallerRole == LootLockerCallerRole.Admin) - { - #if UNITY_EDITOR - executionItem.RequestData.ExtraHeaders["x-auth-token"] = LootLockerConfig.current.adminToken; - #endif - } - else - { - executionItem.RequestData.ExtraHeaders["x-session-token"] = tokenAfterRefresh; - } - - // Mark request as ready for continuation - executionItem.IsWaitingForSessionRefresh = false; - } - } - })); - + CurrentlyOngoingRequests.Remove(executionItem.RequestData.RequestId); + executionItem.IsWaitingForSessionRefresh = true; + executionItem.RequestData.TimesRetried++; + // Unsetting web request fields will make the execution queue retry it + executionItem.AbortRequest(); + + StartCoroutine(RefreshSession(executionItem.RequestData.ForPlayerWithUlid, executionItemId, HandleSessionRefreshResult)); + } } } @@ -497,7 +463,6 @@ private HTTPExecutionQueueProcessingResult ProcessOngoingRequest(LootLockerHTTPE { if (ShouldRefreshSession(executionItem.WebRequest.responseCode, playerData == null ? LL_AuthPlatforms.None : playerData.CurrentPlatform.Platform) && (CanRefreshUsingRefreshToken(executionItem.RequestData) || CanStartNewSessionUsingCachedAuthData(executionItem.RequestData.ForPlayerWithUlid))) { - executionItem.IsWaitingForSessionRefresh = true; return HTTPExecutionQueueProcessingResult.NeedsSessionRefresh; } return HTTPExecutionQueueProcessingResult.ShouldBeRetried; @@ -605,13 +570,13 @@ private void CallListenersAndMarkDone(LootLockerHTTPExecutionQueueItem execution CompletedRequestIDs.Add(executionItem.RequestData.RequestId); } - private IEnumerator RefreshSession(string refreshForPlayerUlid, Action onSessionRefreshedCallback) + private IEnumerator RefreshSession(string refreshForPlayerUlid, string forExecutionItemId, Action onSessionRefreshedCallback) { var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(refreshForPlayerUlid); if (playerData == null) { LootLockerLogger.Log($"No stored player data for player with ulid {refreshForPlayerUlid}. Can't refresh session.", LootLockerLogger.LogLevel.Warning); - onSessionRefreshedCallback?.Invoke(LootLockerResponseFactory.Failure(401, $"No stored player data for player with ulid {refreshForPlayerUlid}. Can't refresh session.", refreshForPlayerUlid)); + onSessionRefreshedCallback?.Invoke(LootLockerResponseFactory.Failure(401, $"No stored player data for player with ulid {refreshForPlayerUlid}. Can't refresh session.", refreshForPlayerUlid), refreshForPlayerUlid, forExecutionItemId); yield break; } @@ -715,7 +680,45 @@ private IEnumerator RefreshSession(string refreshForPlayerUlid, Action callCompleted); - onSessionRefreshedCallback?.Invoke(newSessionResponse); + onSessionRefreshedCallback?.Invoke(newSessionResponse, refreshForPlayerUlid, forExecutionItemId); + } + + private void HandleSessionRefreshResult(LootLockerResponse newSessionResponse, string forPlayerWithUlid, string forExecutionItemId) + { + if (HTTPExecutionQueue.TryGetValue(forExecutionItemId, out var executionItem)) + { + if (!executionItem.RequestData.ForPlayerWithUlid.Equals(forPlayerWithUlid)) + { + // This refresh callback was not for this user + LootLockerLogger.Log($"Session refresh callback ulid {forPlayerWithUlid} does not match the execution item ulid {executionItem.RequestData.ForPlayerWithUlid}. Ignoring.", LootLockerLogger.LogLevel.Error); + return; + } + var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(executionItem.RequestData.ForPlayerWithUlid); + string tokenBeforeRefresh = executionItem.RequestData.ExtraHeaders["x-session-token"]; + string tokenAfterRefresh = playerData?.SessionToken; + if (string.IsNullOrEmpty(tokenAfterRefresh) || tokenBeforeRefresh.Equals(playerData.SessionToken)) + { + // Session refresh failed so abort call chain + CallListenersAndMarkDone(executionItem, LootLockerResponseFactory.TokenExpiredError(executionItem.RequestData.ForPlayerWithUlid)); + return; + } + + // Session refresh worked so update the session token header + if (executionItem.RequestData.CallerRole == LootLockerCallerRole.Admin) + { +#if UNITY_EDITOR + executionItem.RequestData.ExtraHeaders["x-auth-token"] = LootLockerConfig.current.adminToken; +#endif + } + else + { + executionItem.RequestData.ExtraHeaders["x-session-token"] = tokenAfterRefresh; + } + + // Mark request as ready for continuation + ExecutionItemsNeedingRefresh.Remove(forExecutionItemId); + executionItem.IsWaitingForSessionRefresh = false; + } } #region Session Refresh Helper Methods diff --git a/Runtime/Game/LootLockerLogger.cs b/Runtime/Game/LootLockerLogger.cs index 8fd1b6a3..cd7b4907 100644 --- a/Runtime/Game/LootLockerLogger.cs +++ b/Runtime/Game/LootLockerLogger.cs @@ -213,6 +213,17 @@ public static void LogHttpRequestResponse(LootLockerHttpLogEntry entry) _instance.httpLogRecords[_instance.nextHttpLogRecordWrite] = entry; _instance.nextHttpLogRecordWrite = (_instance.nextHttpLogRecordWrite + 1) % HttpLogBacklogSize; + if( entry == null ) + { + Log("LootLockerLogger.LogHttpRequestResponse called with null entry", LogLevel.Warning); + return; + } + else if (entry.Response == null) + { + Log($"LootLockerLogger.LogHttpRequestResponse called with null entry. Response for {entry.Method} {entry.Url}", LogLevel.Error); + return; + } + // Construct log string for Unity log var sb = new System.Text.StringBuilder(); if (entry.Response?.success ?? false) From 6b65dd42d4d0670696d0007910b1bf400ec43de9 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Tue, 9 Sep 2025 08:52:58 +0200 Subject: [PATCH 12/22] fix: Make WhiteLabelLoginAndStartSesssion use the same email for both calls --- Runtime/Game/LootLockerSDKManager.cs | 2 +- .../PlayMode/WhiteLabelLoginTest.cs | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index cbfffd9f..a16b10cc 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -2390,7 +2390,7 @@ public static void WhiteLabelLoginAndStartSession(string email, string password, onComplete?.Invoke(LootLockerWhiteLabelLoginAndStartSessionResponse.MakeWhiteLabelLoginAndStartSessionResponse(loginResponse, null)); return; } - StartWhiteLabelSession(sessionResponse => + StartWhiteLabelSession(email, sessionResponse => { onComplete?.Invoke(LootLockerWhiteLabelLoginAndStartSessionResponse.MakeWhiteLabelLoginAndStartSessionResponse(loginResponse, sessionResponse)); }); diff --git a/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs b/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs index 2c84a969..97a2395e 100644 --- a/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs +++ b/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs @@ -362,5 +362,72 @@ public IEnumerator WhiteLabel_WLSessionStartByEmailAfterGameReset_ReusesSession( Assert.AreNotEqual(initialPlayerSessionToken, playerData.SessionToken, "New session token was not generated"); } + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator WhiteLabel_WLLoginAndStartSessionWhenOtherPlayerExists_CreatesWhiteLabelPlayer() + { + //Given + string otherEmail = GetRandomName() + "@lootlocker.com"; + string email = GetRandomName() + "@lootlocker.com"; + + Assert.AreNotEqual(otherEmail, email, "Emails were randomized to the same value, test will not work"); + + LootLockerWhiteLabelSignupResponse signupResponse = null; + bool whiteLabelSignUpCompleted = false; + LootLockerSDKManager.WhiteLabelSignUp(otherEmail, "123456789", (response) => + { + + signupResponse = response; + whiteLabelSignUpCompleted = true; + }); + yield return new WaitUntil(() => whiteLabelSignUpCompleted); + Assert.IsTrue(signupResponse.success, "Could not sign up other player with WhiteLabel"); + + LootLockerWhiteLabelLoginAndStartSessionResponse loginResponse = null; + bool whiteLabelLoginCompleted = false; + LootLockerSDKManager.WhiteLabelLoginAndStartSession(otherEmail, "123456789", true, (response) => + { + loginResponse = response; + whiteLabelLoginCompleted = true; + }); + yield return new WaitUntil(() => whiteLabelLoginCompleted); + + Assert.IsTrue(loginResponse.SessionResponse.success, "Could not start other player White Label Session"); + Assert.IsNotEmpty(loginResponse.SessionResponse.session_token, "No session token found from login"); + string otherPlayerUlid = loginResponse?.SessionResponse?.player_ulid; + string otherPlayerSessionToken = loginResponse?.SessionResponse?.session_token; + string otherPlayerWLEmail = loginResponse?.LoginResponse?.Email; + + signupResponse = null; + whiteLabelSignUpCompleted = false; + LootLockerSDKManager.WhiteLabelSignUp(email, "123456789", (response) => + { + + signupResponse = response; + whiteLabelSignUpCompleted = true; + }); + yield return new WaitUntil(() => whiteLabelSignUpCompleted); + + Assert.IsTrue(signupResponse.success, "Could not sign up with WhiteLabel"); + + //When + loginResponse = null; + whiteLabelLoginCompleted = false; + LootLockerSDKManager.WhiteLabelLoginAndStartSession(email, "123456789", true, (response) => + { + loginResponse = response; + whiteLabelLoginCompleted = true; + }); + yield return new WaitUntil(() => whiteLabelLoginCompleted); + + // Then + Assert.IsTrue(loginResponse.SessionResponse.success, "Could not start White Label Session"); + Assert.IsNotEmpty(loginResponse.SessionResponse.session_token, "No session token found from login"); + Assert.AreNotEqual(otherPlayerUlid, loginResponse?.SessionResponse?.player_ulid, "WL login did not create new player"); + Assert.AreNotEqual(otherPlayerSessionToken, loginResponse?.SessionResponse?.session_token, "WL login did not create new session"); + Assert.AreNotEqual(otherPlayerWLEmail, loginResponse?.LoginResponse?.Email, "WL login did not create new WL user"); + Assert.AreEqual(email, loginResponse?.LoginResponse?.Email, "WL login email does not match"); + Assert.AreEqual(otherPlayerUlid, LootLockerSDKManager.GetDefaultPlayerUlid(), "Default player ULID was changed by WL login"); + } + } } \ No newline at end of file From 2d1b37968b444c6ffef04323a58eca61e0d2e9d1 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Tue, 9 Sep 2025 09:25:06 +0200 Subject: [PATCH 13/22] feat: Add last_seen to session and player info responses --- Runtime/Game/Requests/LootLockerSessionRequest.cs | 4 ++++ Runtime/Game/Requests/PlayerRequest.cs | 4 ++++ Tests/LootLockerTests/PlayMode/GuestSessionTest.cs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/Runtime/Game/Requests/LootLockerSessionRequest.cs b/Runtime/Game/Requests/LootLockerSessionRequest.cs index 50582393..f0469b39 100644 --- a/Runtime/Game/Requests/LootLockerSessionRequest.cs +++ b/Runtime/Game/Requests/LootLockerSessionRequest.cs @@ -78,6 +78,10 @@ public class LootLockerSessionResponse : LootLockerResponse /// public bool seen_before { get; set; } /// + /// The last time this player logged in + /// + public DateTime last_seen { get; set; } + /// /// The public UID for this player /// public string public_uid { get; set; } diff --git a/Runtime/Game/Requests/PlayerRequest.cs b/Runtime/Game/Requests/PlayerRequest.cs index 04f1c0c3..ea6ce362 100644 --- a/Runtime/Game/Requests/PlayerRequest.cs +++ b/Runtime/Game/Requests/PlayerRequest.cs @@ -93,6 +93,10 @@ public class LootLockerPlayerInfo /// public DateTime created_at { get; set; } /// + /// The last time this player logged in + /// + public DateTime last_seen { get; set; } + /// /// The name of the player expressly configured through a SetPlayerName call /// public string name { get; set; } diff --git a/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs b/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs index f7d619d9..ed4ec27d 100644 --- a/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs +++ b/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs @@ -207,10 +207,12 @@ public IEnumerator StartGuestSession_WithStoredIdentifier_Succeeds() //Given LootLockerGuestSessionResponse actualResponse = null; string expectedIdentifier = null; + DateTime lastSeenOnFirstSession = DateTime.MinValue; bool firstSessionCompleted = false; LootLockerSDKManager.StartGuestSession((startSessionResponse) => { expectedIdentifier = startSessionResponse.player_identifier; + lastSeenOnFirstSession = startSessionResponse.last_seen; LootLockerSDKManager.EndSession((endSessionResponse) => { @@ -230,6 +232,8 @@ public IEnumerator StartGuestSession_WithStoredIdentifier_Succeeds() //Then Assert.IsTrue(actualResponse.success, "Guest Session failed to start"); + Assert.AreEqual(lastSeenOnFirstSession, DateTime.MinValue, "last_seen on first session was set"); + Assert.Greater(actualResponse.last_seen, DateTime.MinValue, "last_seen on second session was not set"); Assert.AreEqual(expectedIdentifier, actualResponse.player_identifier, "Guest Session using stored Identifier failed to work"); } From 565c069da5abd1ecf1b5c88c932ab0dc7d18bccb Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Tue, 9 Sep 2025 11:19:59 +0200 Subject: [PATCH 14/22] feat: Add increment score support --- Runtime/Client/LootLockerEndPoints.cs | 1 + Runtime/Game/LootLockerSDKManager.cs | 28 ++++ Runtime/Game/Requests/LeaderboardRequest.cs | 6 + .../PlayMode/LeaderboardTest.cs | 154 ++++++++++++++++++ 4 files changed, 189 insertions(+) diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 05e0c36e..6e87aeb5 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -234,6 +234,7 @@ public class LootLockerEndPoints public static EndPointClass getAllMemberRanks = new EndPointClass("leaderboards/member/{0}?count={1}", LootLockerHTTPMethod.GET); public static EndPointClass getScoreList = new EndPointClass("leaderboards/{0}/list?count={1}", LootLockerHTTPMethod.GET); public static EndPointClass submitScore = new EndPointClass("leaderboards/{0}/submit", LootLockerHTTPMethod.POST); + public static EndPointClass incrementScore = new EndPointClass("leaderboards/{0}/increment", LootLockerHTTPMethod.POST); public static EndPointClass getLeaderboardData = new EndPointClass("leaderboards/{0}/info", LootLockerHTTPMethod.GET); public static EndPointClass listLeaderboardArchive = new EndPointClass("leaderboards/{0}/archive/list", LootLockerHTTPMethod.GET); public static EndPointClass getLeaderboardArchive = new EndPointClass("leaderboards/archive/read?key={0}&", LootLockerHTTPMethod.GET); diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index a16b10cc..5494b34b 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6460,6 +6460,34 @@ public static void SubmitScore(string memberId, int score, string leaderboardKey LootLockerAPIManager.SubmitScore(forPlayerWithUlid, request, leaderboardKey, onComplete); } + /// + /// Increment an existing score on a leaderboard by the given amount. + /// + /// Can be left blank if it is a player leaderboard, otherwise this is the identifier you wish to use for this score + /// The amount with which to increment the current score on the given leaderboard (can be positive or negative) + /// Key of the leaderboard to submit score to + /// onComplete Action for handling the response of type LootLockerSubmitScoreResponse + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void IncrementScore(string memberId, int amount, string leaderboardKey, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + LootLockerIncrementScoreRequest request = new LootLockerIncrementScoreRequest(); + request.member_id = memberId; + request.amount = amount; + + EndPointClass requestEndPoint = LootLockerEndPoints.incrementScore; + + string json = LootLockerJson.SerializeObject(request); + + string endPoint = requestEndPoint.WithPathParameter(leaderboardKey); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint, requestEndPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + /// /// List the archived versions of a leaderboard, containing past rewards, ranks, etc. /// diff --git a/Runtime/Game/Requests/LeaderboardRequest.cs b/Runtime/Game/Requests/LeaderboardRequest.cs index eef3bc4c..ca0c1890 100644 --- a/Runtime/Game/Requests/LeaderboardRequest.cs +++ b/Runtime/Game/Requests/LeaderboardRequest.cs @@ -171,6 +171,12 @@ public class LootLockerSubmitScoreRequest public string metadata { get; set; } } + public class LootLockerIncrementScoreRequest + { + public string member_id { get; set; } + public int amount { get; set; } + } + [Serializable] public class LootLockerLeaderboardArchiveRequest { diff --git a/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs b/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs index b2a3b526..63db4ac4 100644 --- a/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs +++ b/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs @@ -320,5 +320,159 @@ public IEnumerator Leaderboard_ListScoresThatHaveMetadata_GetsMetadata() } } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator Leaderboard_IncrementScorePositive_Succeeds() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + //Given + int initialScore = 100; + bool scoreSubmitted = false; + LootLockerSDKManager.SubmitScore(null, initialScore, leaderboardKey, (response) => + { + scoreSubmitted = true; + }); + yield return new WaitUntil(() => scoreSubmitted); + + //When + LootLockerSubmitScoreResponse incrementResponse = null; + bool scoreIncremented = false; + int incrementAmount = 10; + LootLockerSDKManager.IncrementScore(null, incrementAmount, leaderboardKey, (response) => + { + incrementResponse = response; + scoreIncremented = true; + }); + yield return new WaitUntil(() => scoreIncremented); + + // Then + LootLockerGetScoreListResponse actualResponse = null; + bool scoreListCompleted = false; + LootLockerSDKManager.GetScoreList(leaderboardKey, 1, 0, (response) => + { + actualResponse = response; + scoreListCompleted = true; + }); + yield return new WaitUntil(() => scoreListCompleted); + + //Then + Assert.IsTrue(incrementResponse.success, "IncrementScore request failed"); + Assert.IsTrue(actualResponse.success, "GetScoreList request failed"); + Assert.AreEqual(1, actualResponse.items.Length, "Did not get the expected number of scores"); + Assert.AreEqual(initialScore + incrementAmount, actualResponse.items[0].score, "Score was not incremented as expected"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator Leaderboard_IncrementScoreNegative_Succeeds() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + //Given + int initialScore = 100; + bool scoreSubmitted = false; + LootLockerSDKManager.SubmitScore(null, initialScore, leaderboardKey, (response) => + { + scoreSubmitted = true; + }); + yield return new WaitUntil(() => scoreSubmitted); + + //When + LootLockerSubmitScoreResponse incrementResponse = null; + bool scoreIncremented = false; + int incrementAmount = -10; + LootLockerSDKManager.IncrementScore(null, incrementAmount, leaderboardKey, (response) => + { + incrementResponse = response; + scoreIncremented = true; + }); + yield return new WaitUntil(() => scoreIncremented); + + // Then + LootLockerGetScoreListResponse actualResponse = null; + bool scoreListCompleted = false; + LootLockerSDKManager.GetScoreList(leaderboardKey, 1, 0, (response) => + { + actualResponse = response; + scoreListCompleted = true; + }); + yield return new WaitUntil(() => scoreListCompleted); + + //Then + Assert.IsTrue(incrementResponse.success, "IncrementScore request failed"); + Assert.IsTrue(actualResponse.success, "GetScoreList request failed"); + Assert.AreEqual(1, actualResponse.items.Length, "Did not get the expected number of scores"); + Assert.AreEqual(initialScore + incrementAmount, actualResponse.items[0].score, "Score was not incremented as expected"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator Leaderboard_PositivelyIncrementNonExistingScore_SetsScoreToIncrementValue() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + //Given + int initialScore = 0; + + //When + LootLockerSubmitScoreResponse incrementResponse = null; + bool scoreIncremented = false; + int incrementAmount = 10; + LootLockerSDKManager.IncrementScore(null, incrementAmount, leaderboardKey, (response) => + { + incrementResponse = response; + scoreIncremented = true; + }); + yield return new WaitUntil(() => scoreIncremented); + + // Then + LootLockerGetScoreListResponse actualResponse = null; + bool scoreListCompleted = false; + LootLockerSDKManager.GetScoreList(leaderboardKey, 1, 0, (response) => + { + actualResponse = response; + scoreListCompleted = true; + }); + yield return new WaitUntil(() => scoreListCompleted); + + //Then + Assert.IsTrue(incrementResponse.success, "IncrementScore request failed"); + Assert.IsTrue(actualResponse.success, "GetScoreList request failed"); + Assert.AreEqual(1, actualResponse.items.Length, "Did not get the expected number of scores"); + Assert.AreEqual(initialScore + incrementAmount, actualResponse.items[0].score, "Score was not incremented as expected"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator Leaderboard_NegativelyIncrementNonExistingScore_Leaderboard_PositivelyIncrementNonExistingScore_SetsScoreToIncrementValue() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + //Given + + //When + LootLockerSubmitScoreResponse incrementResponse = null; + bool scoreIncremented = false; + int incrementAmount = -10; + LootLockerSDKManager.IncrementScore(null, incrementAmount, leaderboardKey, (response) => + { + incrementResponse = response; + scoreIncremented = true; + }); + yield return new WaitUntil(() => scoreIncremented); + + // Then + LootLockerGetScoreListResponse actualResponse = null; + bool scoreListCompleted = false; + LootLockerSDKManager.GetScoreList(leaderboardKey, 1, 0, (response) => + { + actualResponse = response; + scoreListCompleted = true; + }); + yield return new WaitUntil(() => scoreListCompleted); + + //Then + Assert.IsFalse(incrementResponse.success, "IncrementScore request failed"); + Assert.IsTrue(actualResponse.success, "GetScoreList request failed"); + Assert.AreEqual(0, actualResponse.items.Length, "Did not get the expected number of scores"); + } } } From a48060b3577a8549cb181c375d2083c80cc65e57 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Tue, 9 Sep 2025 11:56:01 +0200 Subject: [PATCH 15/22] feat: Add support for querying a score --- Runtime/Client/LootLockerEndPoints.cs | 1 + Runtime/Game/LootLockerSDKManager.cs | 26 ++++ Runtime/Game/Requests/LeaderboardRequest.cs | 5 + .../PlayMode/LeaderboardTest.cs | 135 +++++++++++++++++- 4 files changed, 163 insertions(+), 4 deletions(-) diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 6e87aeb5..42e99918 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -235,6 +235,7 @@ public class LootLockerEndPoints public static EndPointClass getScoreList = new EndPointClass("leaderboards/{0}/list?count={1}", LootLockerHTTPMethod.GET); public static EndPointClass submitScore = new EndPointClass("leaderboards/{0}/submit", LootLockerHTTPMethod.POST); public static EndPointClass incrementScore = new EndPointClass("leaderboards/{0}/increment", LootLockerHTTPMethod.POST); + public static EndPointClass queryScore = new EndPointClass("leaderboards/{0}/query", LootLockerHTTPMethod.POST); public static EndPointClass getLeaderboardData = new EndPointClass("leaderboards/{0}/info", LootLockerHTTPMethod.GET); public static EndPointClass listLeaderboardArchive = new EndPointClass("leaderboards/{0}/archive/list", LootLockerHTTPMethod.GET); public static EndPointClass getLeaderboardArchive = new EndPointClass("leaderboards/archive/read?key={0}&", LootLockerHTTPMethod.GET); diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 5494b34b..e1feefeb 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6460,6 +6460,32 @@ public static void SubmitScore(string memberId, int score, string leaderboardKey LootLockerAPIManager.SubmitScore(forPlayerWithUlid, request, leaderboardKey, onComplete); } + /// + /// Query a leaderboard for which rank a specific score would achieve. Does not submit the score but returns the projected rank. + /// + /// The score to use for the query + /// Key of the leaderboard to submit score to + /// onComplete Action for handling the response of type LootLockerSubmitScoreResponse + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void QueryScore(int score, string leaderboardKey, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + LootLockerQueryScoreRequest request = new LootLockerQueryScoreRequest(); + request.score = score; + + EndPointClass requestEndPoint = LootLockerEndPoints.queryScore; + + string json = LootLockerJson.SerializeObject(request); + + string endPoint = requestEndPoint.WithPathParameter(leaderboardKey); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint, requestEndPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + /// /// Increment an existing score on a leaderboard by the given amount. /// diff --git a/Runtime/Game/Requests/LeaderboardRequest.cs b/Runtime/Game/Requests/LeaderboardRequest.cs index ca0c1890..82abc238 100644 --- a/Runtime/Game/Requests/LeaderboardRequest.cs +++ b/Runtime/Game/Requests/LeaderboardRequest.cs @@ -171,6 +171,11 @@ public class LootLockerSubmitScoreRequest public string metadata { get; set; } } + public class LootLockerQueryScoreRequest + { + public int score { get; set; } + } + public class LootLockerIncrementScoreRequest { public string member_id { get; set; } diff --git a/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs b/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs index 63db4ac4..ff5aac31 100644 --- a/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs +++ b/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs @@ -442,12 +442,12 @@ public IEnumerator Leaderboard_PositivelyIncrementNonExistingScore_SetsScoreToIn } [UnityTest, Category("LootLocker"), Category("LootLockerCI")] - public IEnumerator Leaderboard_NegativelyIncrementNonExistingScore_Leaderboard_PositivelyIncrementNonExistingScore_SetsScoreToIncrementValue() + public IEnumerator Leaderboard_NegativelyIncrementNonExistingScore_SetsScoreToIncrementValue() { Assert.IsFalse(SetupFailed, "Failed to setup game"); //Given - + int initialScore = 0; //When LootLockerSubmitScoreResponse incrementResponse = null; bool scoreIncremented = false; @@ -470,9 +470,136 @@ public IEnumerator Leaderboard_NegativelyIncrementNonExistingScore_Leaderboard_P yield return new WaitUntil(() => scoreListCompleted); //Then - Assert.IsFalse(incrementResponse.success, "IncrementScore request failed"); + Assert.IsTrue(incrementResponse.success, "IncrementScore request failed"); Assert.IsTrue(actualResponse.success, "GetScoreList request failed"); - Assert.AreEqual(0, actualResponse.items.Length, "Did not get the expected number of scores"); + Assert.AreEqual(1, actualResponse.items.Length, "Did not get the expected number of scores"); + Assert.AreEqual(initialScore + incrementAmount, actualResponse.items[0].score, "Score was not incremented as expected"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator Leaderboard_QueryScoreForPlayerTypeLeaderboard_GivesCorrectPlacement() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + //Given + int submittedScores = 0; + List Users = new List { }; + List Scores = new List { }; + for (; submittedScores < 10; submittedScores++) + { + bool guestSessionCompleted = false; + string user = $"User_number_{submittedScores}"; + string playerUlid = ""; + LootLockerSDKManager.StartGuestSession(user, (response) => + { + playerUlid = response.player_ulid; + guestSessionCompleted = true; + }); + yield return new WaitUntil(() => guestSessionCompleted); + + Users.Add(playerUlid); + bool scoreSubmitted = false; + int score = (submittedScores + 1) * 100; + LootLockerSDKManager.SubmitScore(null, score, leaderboardKey, (response) => + { + scoreSubmitted = true; + }, playerUlid); + yield return new WaitUntil(() => scoreSubmitted); + Scores.Add(score); + } + + //When + + int expectedRank = 5; + int scoreToQuery = Scores[Scores.Count - expectedRank] + 10; // Scores are in increasing order so reverse index + LootLockerSubmitScoreResponse queryResponse = null; + bool scoreQueried = false; + LootLockerSDKManager.QueryScore(scoreToQuery, leaderboardKey, (response) => + { + queryResponse = response; + scoreQueried = true; + }); + yield return new WaitUntil(() => scoreQueried); + + //Then + Assert.IsTrue(queryResponse.success, "QueryScore request failed"); + Assert.AreEqual(expectedRank, queryResponse.rank, "Did not get expected rank"); + Assert.AreEqual(scoreToQuery, queryResponse.score, "Did not get expected score"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator Leaderboard_QueryScoreForGenericTypeLeaderboard_GivesCorrectPlacement() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + //Given + string genericLeaderboardKey = "genericleaderboard"; + var createLeaderboardRequest = new CreateLootLockerLeaderboardRequest + { + name = "Local Generic Leaderboard", + key = genericLeaderboardKey, + direction_method = LootLockerLeaderboardSortDirection.descending.ToString(), + enable_game_api_writes = true, + has_metadata = true, + overwrite_score_on_submit = false, + type = "generic" + }; + + bool leaderboardCreated = false; + bool leaderboardSuccess = false; + gameUnderTest.CreateLeaderboard(createLeaderboardRequest, (response) => + { + leaderboardSuccess = response.success; + leaderboardCreated = true; + + }); + yield return new WaitUntil(() => leaderboardCreated); + Assert.IsTrue(leaderboardSuccess, "Could not create generic leaderboard"); + + int submittedScores = 0; + List Users = new List { }; + List Scores = new List { }; + for (; submittedScores < 10; submittedScores++) + { + bool guestSessionCompleted = false; + string user = $"User_number_{submittedScores}"; + string playerUlid = ""; + string playerPublicUID = ""; + LootLockerSDKManager.StartGuestSession(user, (response) => + { + playerUlid = response.player_ulid; + playerPublicUID = response.public_uid; + guestSessionCompleted = true; + }); + yield return new WaitUntil(() => guestSessionCompleted); + + Users.Add(playerUlid); + bool scoreSubmitted = false; + int score = (submittedScores + 1) * 100; + LootLockerSDKManager.SubmitScore(playerPublicUID, score, genericLeaderboardKey, (response) => + { + scoreSubmitted = true; + }, playerUlid); + yield return new WaitUntil(() => scoreSubmitted); + Scores.Add(score); + } + + //When + int expectedRank = 5; + int scoreToQuery = Scores[Scores.Count - expectedRank] + 10; // Scores are in increasing order so reverse index + LootLockerSubmitScoreResponse queryResponse = null; + bool scoreQueried = false; + LootLockerSDKManager.QueryScore(scoreToQuery, genericLeaderboardKey, (response) => + { + queryResponse = response; + scoreQueried = true; + }); + yield return new WaitUntil(() => scoreQueried); + + //Then + Assert.IsTrue(queryResponse.success, "QueryScore request failed"); + Assert.AreEqual(expectedRank, queryResponse.rank, "Did not get expected rank"); + Assert.AreEqual(scoreToQuery, queryResponse.score, "Did not get expected score"); } } } From c9aacd5f94e41241f4cf122f350254d3b03254e6 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Wed, 10 Sep 2025 16:20:44 +0200 Subject: [PATCH 16/22] Bump version to v6.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0bf1e55d..470602ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.lootlocker.lootlockersdk", - "version": "5.3.0", + "version": "6.0.0", "displayName": "LootLocker", "description": "LootLocker is a game backend-as-a-service with plug and play tools to upgrade your game and give your players the best experience possible. Designed for teams of all shapes and sizes, on mobile, PC and console. From solo developers, indie teams, AAA studios, and publishers. Built with cross-platform in mind.\n\n▪ Manage your game\nSave time and upgrade your game with leaderboards, progression, and more. Completely off-the-shelf features, built to work with any game and platform.\n\n▪ Manage your content\nTake charge of your game's content on all platforms, in one place. Sort, edit and manage everything, from cosmetics to currencies, UGC to DLC. Without breaking a sweat.\n\n▪ Manage your players\nStore your players' data together in one place. Access their profile and friends list cross-platform. Manage reports, messages, refunds and gifts to keep them hooked.\n", "unity": "2019.2", From ac8450a3357d510e2e00240ebc385a85674b983b Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 12 Sep 2025 09:54:34 +0200 Subject: [PATCH 17/22] feat: Add support for deleting characters --- Runtime/Client/LootLockerEndPoints.cs | 1 + Runtime/Game/LootLockerSDKManager.cs | 17 +++++++++++++++++ Runtime/Game/Requests/ClassRequests.cs | 7 +++++++ 3 files changed, 25 insertions(+) diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 42e99918..55a82326 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -99,6 +99,7 @@ public class LootLockerEndPoints public static EndPointClass getEquipableContextToDefaultClass = new EndPointClass("v1/player/character/contexts", LootLockerHTTPMethod.GET); public static EndPointClass getEquipableContextbyClass = new EndPointClass("v1/player/character/{0}/contexts", LootLockerHTTPMethod.GET); public static EndPointClass createClass = new EndPointClass("v1/player/character", LootLockerHTTPMethod.POST); + public static EndPointClass deleteClass = new EndPointClass("v1/player/character/{0}", LootLockerHTTPMethod.DELETE); public static EndPointClass listClassTypes = new EndPointClass("v1/player/character/types", LootLockerHTTPMethod.GET); public static EndPointClass listPlayerClasses = new EndPointClass("v1/player/character/list", LootLockerHTTPMethod.GET); diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index e1feefeb..14b2145e 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -3840,6 +3840,23 @@ public static void CreateClass(string classTypeID, string newClassName, bool isD LootLockerAPIManager.CreateClass(forPlayerWithUlid, data, onComplete); } + /// + /// Delete a Class with the provided classId. The Class will be removed from the currently active player. + /// + /// The id of the class you want to delete + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void DeleteClass(int classId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.DeleteClass(forPlayerWithUlid, classId, onComplete); + } + /// /// List all available Class types for your game. /// diff --git a/Runtime/Game/Requests/ClassRequests.cs b/Runtime/Game/Requests/ClassRequests.cs index 140698d3..819da59c 100644 --- a/Runtime/Game/Requests/ClassRequests.cs +++ b/Runtime/Game/Requests/ClassRequests.cs @@ -174,6 +174,13 @@ public static void CreateClass(string forPlayerWithUlid, LootLockerCreateClassRe LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } + public static void DeleteClass(string forPlayerWithUlid, int classId, Action onComplete) + { + EndPointClass endPoint = LootLockerEndPoints.deleteClass; + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.WithPathParameter(classId), endPoint.httpMethod, null, (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + public static void ListClassTypes(string forPlayerWithUlid, Action onComplete) { EndPointClass endPoint = LootLockerEndPoints.listClassTypes; From 135df6bb829131a81532e10f863da642016d907d Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Wed, 10 Sep 2025 16:58:43 +0200 Subject: [PATCH 18/22] feat: Add progression id to progression responses --- Runtime/Game/Requests/ProgressionsRequest.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Runtime/Game/Requests/ProgressionsRequest.cs b/Runtime/Game/Requests/ProgressionsRequest.cs index 6d1a09b8..5754c3d4 100644 --- a/Runtime/Game/Requests/ProgressionsRequest.cs +++ b/Runtime/Game/Requests/ProgressionsRequest.cs @@ -29,6 +29,7 @@ public class LootLockerPlayerProgressionResponse : LootLockerResponse { public string id { get; set; } public string progression_key { get; set; } + public string progression_id { get; set; } public string progression_name { get; set; } public ulong step { get; set; } public ulong points { get; set; } @@ -46,6 +47,7 @@ public class LootLockerPlayerProgression { public string id { get; set; } public string progression_key { get; set; } + public string progression_id { get; set; } public string progression_name { get; set; } public ulong step { get; set; } public ulong points { get; set; } @@ -64,6 +66,7 @@ public class LootLockerCharacterProgressionResponse : LootLockerResponse { public string id { get; set; } public string progression_key { get; set; } + public string progression_id { get; set; } public string progression_name { get; set; } public ulong step { get; set; } public ulong points { get; set; } @@ -81,6 +84,7 @@ public class LootLockerCharacterProgression { public string id { get; set; } public string progression_key { get; set; } + public string progression_id { get; set; } public string progression_name { get; set; } public ulong step { get; set; } public ulong points { get; set; } @@ -99,6 +103,7 @@ public class LootLockerAssetInstanceProgressionResponse : LootLockerResponse { public string id { get; set; } public string progression_key { get; set; } + public string progression_id { get; set; } public string progression_name { get; set; } public ulong step { get; set; } public ulong points { get; set; } @@ -116,6 +121,7 @@ public class LootLockerAssetInstanceProgression { public string id { get; set; } public string progression_key { get; set; } + public string progression_id { get; set; } public string progression_name { get; set; } public ulong step { get; set; } public ulong points { get; set; } From e3080720cbd6127d3bd966daa3567165bf661dbe Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Thu, 11 Sep 2025 16:22:47 +0200 Subject: [PATCH 19/22] feat: Add pagination to friends & followers and get friend --- Runtime/Client/LootLockerEndPoints.cs | 1 + Runtime/Game/LootLockerSDKManager.cs | 202 +++++++++++++++++++++--- Runtime/Game/Requests/FriendsRequest.cs | 46 ++++++ 3 files changed, 224 insertions(+), 25 deletions(-) diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 55a82326..f22dbf07 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -293,6 +293,7 @@ public class LootLockerEndPoints // Friends [Header("Friends")] public static EndPointClass listFriends = new EndPointClass("player/friends", LootLockerHTTPMethod.GET); + public static EndPointClass getFriend = new EndPointClass("player/friends/{0}", LootLockerHTTPMethod.GET); public static EndPointClass listIncomingFriendReqeusts = new EndPointClass("player/friends/incoming", LootLockerHTTPMethod.GET); public static EndPointClass listOutgoingFriendRequests = new EndPointClass("player/friends/outgoing", LootLockerHTTPMethod.GET); public static EndPointClass sendFriendRequest = new EndPointClass("player/friends/{0}", LootLockerHTTPMethod.POST); diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 14b2145e..c2548688 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6869,11 +6869,23 @@ private static void SendFeedback(LootLockerFeedbackTypes type, string ulid, stri #region Friends /// - /// List friends for the currently logged in player + /// List friends for the currently logged in player with default pagination /// /// onComplete Action for handling the response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void ListFriends(Action onComplete, string forPlayerWithUlid = null) + { + ListFriendsPaginated(0, 0, onComplete, forPlayerWithUlid); + } + + /// + /// List friends for the currently logged in player + /// + /// The number of results to return per page + /// The page number to return + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListFriendsPaginated(int PerPage, int Page, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { @@ -6881,15 +6893,55 @@ public static void ListFriends(Action onComplete, return; } - LootLockerServerRequest.CallAPI(forPlayerWithUlid, LootLockerEndPoints.listFriends.endPoint, LootLockerEndPoints.listFriends.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + if (Page > 0) + queryParams.Add("page", Page.ToString()); + if (PerPage > 0) + queryParams.Add("per_page", PerPage.ToString()); + + string endpointWithParams = LootLockerEndPoints.listFriends.endPoint + queryParams.ToString(); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpointWithParams, LootLockerEndPoints.listFriends.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// - /// List incoming friend requests for the currently logged in player (friend requests made by others for this player) + /// Get a specific friend of the currently logged in player + /// + /// The ULID of the player for whom to get friend information + /// Action for handling the response + /// Optional: Execute the request for the specified player. If not supplied, the default player will be used. + public static void GetFriend(string friendPlayerULID, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + var formattedEndPoint = LootLockerEndPoints.getFriend.WithPathParameter(friendPlayerULID); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, formattedEndPoint, LootLockerEndPoints.getFriend.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// List incoming friend requests for the currently logged in player (friend requests made by others for this player) with default pagination /// /// onComplete Action for handling the response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void ListIncomingFriendRequests(Action onComplete, string forPlayerWithUlid = null) + { + ListIncomingFriendRequestsPaginated(0, 0, onComplete, forPlayerWithUlid); + } + + /// + /// List incoming friend requests for the currently logged in player with pagination (friend requests made by others for this player) + /// + /// The number of results to return per page + /// The page number to retrieve + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + + public static void ListIncomingFriendRequestsPaginated(int PerPage, int Page, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { @@ -6897,7 +6949,15 @@ public static void ListIncomingFriendRequests(Action { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + if (Page > 0) + queryParams.Add("page", Page.ToString()); + if (PerPage > 0) + queryParams.Add("per_page", PerPage.ToString()); + + string endpointWithParams = LootLockerEndPoints.listIncomingFriendReqeusts.endPoint + queryParams.ToString(); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpointWithParams, LootLockerEndPoints.listIncomingFriendReqeusts.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// @@ -6906,6 +6966,19 @@ public static void ListIncomingFriendRequests(ActiononComplete Action for handling the response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void ListOutgoingFriendRequests(Action onComplete, string forPlayerWithUlid = null) + { + ListOutGoingFriendRequestsPaginated(0, 0, onComplete, forPlayerWithUlid); + } + + + /// + /// List outgoing friend requests for the currently logged in player with pagination (friend requests made by this player) + /// + /// The number of results to return per page + /// The page number to retrieve + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListOutGoingFriendRequestsPaginated(int PerPage, int Page, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { @@ -6913,7 +6986,15 @@ public static void ListOutgoingFriendRequests(Action { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + if (Page > 0) + queryParams.Add("page", Page.ToString()); + if (PerPage > 0) + queryParams.Add("per_page", PerPage.ToString()); + + string endpointWithParams = LootLockerEndPoints.listOutgoingFriendRequests.endPoint + queryParams.ToString(); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpointWithParams, LootLockerEndPoints.listOutgoingFriendRequests.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// @@ -7013,11 +7094,23 @@ public static void DeclineFriendRequest(string playerID, Action - /// List the players (if any) that are blocked by the currently logged in player + /// List the players (if any) that are blocked by the currently logged in player with default pagination /// /// onComplete Action for handling the response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void ListBlockedPlayers(Action onComplete, string forPlayerWithUlid = null) + { + ListBlockedPlayersPaginated(0, 0, onComplete, forPlayerWithUlid); + } + + /// + /// List the players (if any) that are blocked by the currently logged in player + /// + /// The number of results to return per page + /// The page number to retrieve + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListBlockedPlayersPaginated(int PerPage, int Page, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { @@ -7025,7 +7118,15 @@ public static void ListBlockedPlayers(Action { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + if (Page > 0) + queryParams.Add("page", Page.ToString()); + if (PerPage > 0) + queryParams.Add("per_page", PerPage.ToString()); + + string endpointWithParams = LootLockerEndPoints.listOutgoingFriendRequests.endPoint + queryParams.ToString(); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpointWithParams, LootLockerEndPoints.listBlockedPlayers.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// @@ -7103,29 +7204,48 @@ public static void DeleteFriend(string playerID, Action - /// List followers of the currently logged in player + /// List followers of the currently logged in player with default pagination /// /// onComplete Action for handling the response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void ListFollowers(Action onComplete, string forPlayerWithUlid = null) { - if (!CheckInitialized(false, forPlayerWithUlid)) - { - onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); - return; - } - var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(forPlayerWithUlid); ListFollowers(playerData.PublicUID, onComplete, forPlayerWithUlid); } + /// + /// List followers of the currently logged in player + /// + /// Used for pagination, if null or empty string it will return the first page. `next_cursor` will be included in the response if there are more pages. + /// The number of results to return counting from the cursor + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListFollowersPaginated(string Cursor, int Count, Action onComplete, string forPlayerWithUlid = null) + { + var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(forPlayerWithUlid); + ListFollowersPaginated(playerData.PublicUID, Cursor, Count, onComplete, forPlayerWithUlid); + } + + /// + /// List followers that the specified player has with default pagination + /// + /// The public UID of the player whose followers to list + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListFollowers(string playerPublicUID, Action onComplete, string forPlayerWithUlid = null) + { + ListFollowersPaginated(playerPublicUID, null, 0, onComplete, forPlayerWithUlid); + } /// /// List followers that the specified player has /// /// The public UID of the player whose followers to list + /// Used for pagination, if null or empty string it will return the first page. `next_cursor` will be included in the response if there are more pages. + /// The number of results to return counting from the cursor /// onComplete Action for handling the response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. - public static void ListFollowers(string playerPublicUID, Action onComplete, string forPlayerWithUlid = null) + public static void ListFollowersPaginated(string playerPublicUID, string Cursor, int Count, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { @@ -7133,36 +7253,62 @@ public static void ListFollowers(string playerPublicUID, Action 0) + queryParams.Add("per_page", Count.ToString()); + formattedEndPoint += queryParams.ToString(); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, formattedEndPoint, LootLockerEndPoints.listFollowers.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// - /// List what players the currently logged in player is following + /// List what players the currently logged in player is following with default pagination /// /// onComplete Action for handling the response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void ListFollowing(Action onComplete, string forPlayerWithUlid = null) { - if (!CheckInitialized(false, forPlayerWithUlid)) - { - onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); - return; - } - var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(forPlayerWithUlid); ListFollowing(playerData.PublicUID, onComplete, forPlayerWithUlid); } /// - /// List players that the specified player is following + /// List what players the currently logged in player is following + /// + /// Used for pagination, if null or empty string it will return the first page. `next_cursor` will be included in the response if there are more pages. + /// The number of results to return counting from the cursoronComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListFollowingPaginated(string Cursor, int Count, Action onComplete, string forPlayerWithUlid = null) + { + var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(forPlayerWithUlid); + ListFollowingPaginated(playerData.PublicUID, Cursor, Count, onComplete, forPlayerWithUlid); + } + + /// + /// List players that the specified player is following with default pagination /// /// The public UID of the player for which to list following players /// onComplete Action for handling the response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void ListFollowing(string playerPublicUID, Action onComplete, string forPlayerWithUlid = null) + { + ListFollowingPaginated(playerPublicUID, null, 0, onComplete, forPlayerWithUlid); + } + + /// + /// List players that the specified player is following + /// + /// The public UID of the player for which to list following players + /// Used for pagination, if null or empty string it will return the first page. `next_cursor` will be included in the response if there are more pages. + /// The number of results to return counting from the cursor + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListFollowingPaginated(string playerPublicUID, string Cursor, int Count, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { @@ -7170,9 +7316,15 @@ public static void ListFollowing(string playerPublicUID, Action 0) + queryParams.Add("per_page", Count.ToString()); + formattedEndPoint += queryParams.ToString(); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, formattedEndPoint, LootLockerEndPoints.listFollowing.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } diff --git a/Runtime/Game/Requests/FriendsRequest.cs b/Runtime/Game/Requests/FriendsRequest.cs index ca21edaf..35d97ad2 100644 --- a/Runtime/Game/Requests/FriendsRequest.cs +++ b/Runtime/Game/Requests/FriendsRequest.cs @@ -76,6 +76,10 @@ public class LootLockerListFriendsResponse : LootLockerResponse /// A list of the friends for the currently logged in player /// public LootLockerAcceptedFriend[] friends { get; set; } + /// + /// Pagination information for the friends list + /// + public LootLockerExtendedPagination pagination { get; set; } } /// @@ -86,6 +90,10 @@ public class LootLockerListIncomingFriendRequestsResponse : LootLockerResponse /// A list of the incoming friend requests for the currently logged in player /// public LootLockerFriend[] incoming { get; set; } + /// + /// Pagination information for the incoming friends list + /// + public LootLockerExtendedPagination pagination { get; set; } } /// @@ -96,6 +104,40 @@ public class LootLockerListOutgoingFriendRequestsResponse : LootLockerResponse /// A list of the outgoing friend requests for the currently logged in player /// public LootLockerFriend[] outgoing { get; set; } + /// + /// Pagination information for the outgoing friends list + /// + public LootLockerExtendedPagination pagination { get; set; } + } + + /// + /// + public class LootLockerGetFriendResponse : LootLockerResponse + { + /// + /// The id of the player + /// + public string player_id { get; set; } + /// + /// The name (if any has been set) of the player + /// + public string name { get; set; } + /// + /// The public uid of the player + /// + public string public_uid { get; set; } + /// + /// When the player's account was created + /// + public DateTime created_at { get; set; } + /// + /// Whether or not the player is currently online + /// + //public bool online { get; set; } + /// + /// When the friend request for this friend was accepted + /// + public DateTime accepted_at { get; set; } } /// @@ -106,6 +148,10 @@ public class LootLockerListBlockedPlayersResponse : LootLockerResponse /// A list of players that the currently logged in player has blocked /// public LootLockerBlockedPlayer[] blocked { get; set; } + /// + /// Pagination information for the blocked players list + /// + public LootLockerExtendedPagination pagination { get; set; } } /// From f63cd85f0cb601fab5706f40505d755b573e4372 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Mon, 15 Sep 2025 11:55:54 +0200 Subject: [PATCH 20/22] ci: Increase Integration Test timeout --- .github/workflows/run-tests-and-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests-and-package.yml b/.github/workflows/run-tests-and-package.yml index 3667707c..c08f2484 100644 --- a/.github/workflows/run-tests-and-package.yml +++ b/.github/workflows/run-tests-and-package.yml @@ -328,7 +328,7 @@ jobs: runs-on: ubuntu-latest if: ${{ vars.ENABLE_INTEGRATION_TESTS == 'true' }} needs: [editor-smoke-test] - timeout-minutes: 15 + timeout-minutes: 20 strategy: fail-fast: false matrix: From 537002d42e654ffdc74e817377d65be352cdd5d7 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Mon, 15 Sep 2025 12:45:57 +0200 Subject: [PATCH 21/22] ci: Increase timeout only for longer ci --- .github/workflows/run-tests-and-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests-and-package.yml b/.github/workflows/run-tests-and-package.yml index c08f2484..215a89ef 100644 --- a/.github/workflows/run-tests-and-package.yml +++ b/.github/workflows/run-tests-and-package.yml @@ -328,7 +328,7 @@ jobs: runs-on: ubuntu-latest if: ${{ vars.ENABLE_INTEGRATION_TESTS == 'true' }} needs: [editor-smoke-test] - timeout-minutes: 20 + timeout-minutes: ${{ (github.event_name == 'pull_request' && github.base_ref == 'main') && 40 || 15 }} strategy: fail-fast: false matrix: From ff3aae4563778e0794b6a14e81bd2fa6c9ba9cdd Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Tue, 16 Sep 2025 10:39:19 +0200 Subject: [PATCH 22/22] fix: Make last_seen nullable --- Runtime/Game/Requests/LootLockerSessionRequest.cs | 2 +- Tests/LootLockerTests/PlayMode/GuestSessionTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Runtime/Game/Requests/LootLockerSessionRequest.cs b/Runtime/Game/Requests/LootLockerSessionRequest.cs index f0469b39..6c89058b 100644 --- a/Runtime/Game/Requests/LootLockerSessionRequest.cs +++ b/Runtime/Game/Requests/LootLockerSessionRequest.cs @@ -80,7 +80,7 @@ public class LootLockerSessionResponse : LootLockerResponse /// /// The last time this player logged in /// - public DateTime last_seen { get; set; } + public DateTime? last_seen { get; set; } /// /// The public UID for this player /// diff --git a/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs b/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs index ed4ec27d..4c858b14 100644 --- a/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs +++ b/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs @@ -212,7 +212,7 @@ public IEnumerator StartGuestSession_WithStoredIdentifier_Succeeds() LootLockerSDKManager.StartGuestSession((startSessionResponse) => { expectedIdentifier = startSessionResponse.player_identifier; - lastSeenOnFirstSession = startSessionResponse.last_seen; + lastSeenOnFirstSession = startSessionResponse.last_seen ?? DateTime.MinValue; LootLockerSDKManager.EndSession((endSessionResponse) => {