diff --git a/.github/scripts/run-tests.ps1 b/.github/scripts/run-tests.ps1 index fe5154e33..18acde9c3 100644 --- a/.github/scripts/run-tests.ps1 +++ b/.github/scripts/run-tests.ps1 @@ -124,6 +124,10 @@ function Initialize-TestProject { if (-not [string]::IsNullOrEmpty($scriptingDefines)) { $psContent += $nl + ' scriptingDefineSymbols:' + $nl + ' 1: ' + $scriptingDefines } + if ($UseLocalEnv) { + # The local devenv is plain http; Unity blocks insecure connections unless this is set. + $psContent += $nl + ' insecureHttpOption: 2' + } [IO.File]::WriteAllText((Join-Path $TempProject 'ProjectSettings\ProjectSettings.asset'), $psContent) } @@ -144,6 +148,46 @@ if (-not [string]::IsNullOrWhiteSpace($CustomProject) -and (Test-Path $CustomPro $ProjectPath = $TempProject } +# Reused and custom projects need the same local settings as freshly initialized projects. +if ($UseLocalEnv) { + $ProjectSettingsFile = Join-Path $ProjectPath "ProjectSettings\ProjectSettings.asset" + if (Test-Path $ProjectSettingsFile) { + $nl = [char]10 + $settingsContent = [IO.File]::ReadAllText($ProjectSettingsFile) + if ($settingsContent -notmatch 'LOOTLOCKER_COMMANDLINE_SETTINGS') { + $defineProperty = [Regex]::Match($settingsContent, '(?m)^ scriptingDefineSymbols:[^\r\n]*(?:\r?\n|$)') + if ($defineProperty.Success) { + $groupOne = [Regex]::Match($settingsContent.Substring($defineProperty.Index + $defineProperty.Length), '\A(?: [^\r\n]*\r?\n)*? 1:[ \t]*[^\r\n]*') + if ($groupOne.Success) { + $start = $defineProperty.Index + $defineProperty.Length + $groupOne.Index + $newGroup = if ($groupOne.Value -match '^ 1:[ \t]*$') { + ' 1: LOOTLOCKER_COMMANDLINE_SETTINGS' + } else { + $groupOne.Value + ';LOOTLOCKER_COMMANDLINE_SETTINGS' + } + $settingsContent = $settingsContent.Remove($start, $groupOne.Length).Insert($start, $newGroup) + } else { + $settingsContent = $settingsContent.Insert($defineProperty.Index + $defineProperty.Length, ' 1: LOOTLOCKER_COMMANDLINE_SETTINGS' + $nl) + } + } else { + $settingsContent = $settingsContent.TrimEnd() + $nl + ' scriptingDefineSymbols:' + $nl + ' 1: LOOTLOCKER_COMMANDLINE_SETTINGS' + $nl + } + } + if ($settingsContent -match '(?m)^[ \t]*insecureHttpOption:') { + $updatedContent = [Regex]::Replace($settingsContent, '(?m)^[ \t]*insecureHttpOption:.*$', ' insecureHttpOption: 2') + } else { + $updatedContent = $settingsContent.TrimEnd() + $nl + ' insecureHttpOption: 2' + $nl + } + if ($updatedContent -ne $settingsContent) { + [IO.File]::WriteAllText($ProjectSettingsFile, $updatedContent) + Write-Step "Enabled insecure http in $ProjectSettingsFile" + } + } else { + Write-Fail "ProjectSettings.asset not found at $ProjectSettingsFile; cannot enable local http." + exit 1 + } +} + $ResultsFile = Join-Path $ProjectPath "TestResults.xml" $LogFile = Join-Path $ProjectPath "test-run.log" diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 6a5aee8f8..499611a39 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -349,6 +349,16 @@ public class LootLockerEndPoints // Presence (WebSocket) [Header("Presence")] public static EndPointClass presenceWebSocket = new EndPointClass("presence/v1", LootLockerHTTPMethod.GET); + + // Items + [Header("Items")] + public static EndPointClass listItemTemplates = new EndPointClass("player/inventory/v1", LootLockerHTTPMethod.GET); + public static EndPointClass listPlayerInventoryItems = new EndPointClass("player/inventory/v1/my", LootLockerHTTPMethod.GET); + public static EndPointClass getPlayerInventoryItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.GET); + public static EndPointClass deletePlayerInventoryItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.DELETE); + public static EndPointClass consumePlayerInventoryItem = new EndPointClass("player/inventory/v1/{0}/consume", LootLockerHTTPMethod.POST); + public static EndPointClass splitPlayerInventoryItemStack = new EndPointClass("player/inventory/v1/{0}/split", LootLockerHTTPMethod.POST); + public static EndPointClass mergePlayerInventoryItemStacks = new EndPointClass("player/inventory/v1/merge", LootLockerHTTPMethod.POST); } [Serializable] diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 2d104dacf..204a42240 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -3476,6 +3476,11 @@ public static void ListPlayerInfo(string[] playerIdsToLookUp, int[] playerLegacy /// /// Get the players inventory. /// + /// + /// Operates on assets and asset instances. For the item-based inventory, see + /// and friends, which deal with items and + /// item templates and are a separate system. + /// /// Amount of assets to retrieve /// The instance ID the list should start from /// onComplete Action for handling the response of type LootLockerInventoryResponse @@ -3541,6 +3546,11 @@ public static void GetInventory(int count, Action o /// /// List player inventory with default parameters (no filters, first page, default page size). /// + /// + /// Operates on assets and asset instances. For the item-based inventory, see + /// and friends, which deal with items and + /// item templates and are a separate system. + /// /// 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 ListPlayerInventoryWithDefaultParameters(Action onComplete, string forPlayerWithUlid = null) @@ -3564,6 +3574,11 @@ public static void ListPlayerInventoryWithDefaultParameters(ActionOptional : 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. + /// + /// Operates on assets and asset instances. For the item-based inventory, see + /// and friends, which deal with items and + /// item templates and are a separate system. + /// public static void ListPlayerInventory(LootLockerListSimplifiedInventoryRequest request, int perPage, int page, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) @@ -6626,7 +6641,232 @@ public static void DeleteAssetInstanceFromPlayerInventory(int assetInstanceID, A LootLockerAPIManager.DeleteAssetInstanceFromPlayerInventory(forPlayerWithUlid, data, onComplete); } #endregion - + + #region Items + + // These methods operate on items and item templates. They are unrelated to the asset-based + // inventory API (GetInventory, ListPlayerInventory, ...), which deals with assets and asset + // instances. The two share a URL prefix but are separate systems. + + /// + /// Returns a paginated list of all visible item templates. + /// + /// The page of item templates to return. + /// The number of item templates to return per page. + /// 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 ListItemTemplates(int page, int perPage, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + EndPointClass endPoint = LootLockerEndPoints.listItemTemplates; + + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + if (page > 0) + queryParams.Add("page", page); + if (perPage > 0) + queryParams.Add("per_page", perPage); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// Returns a paginated list of the current player's inventory items, including their item templates. + /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API (, , + /// , ), which deals with + /// assets and asset instances. The two share a URL prefix but are separate systems. + /// + /// The page of items to return. + /// The number of items to return per page. + /// (Optional) Return only items whose template name starts with the specified value. Set to null to not use this filter. + /// (Optional) Return only items of the specified type. Set to null to not use this filter. + /// (Optional) Return only items that are (or are not) consumable. Set to null to not use this filter. + /// (Optional) The field by which to sort the items. Set to null to not use this filter. + /// (Optional) The direction in which to sort the items. Set to null to not use this filter. + /// 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 ListPlayerInventoryItems(int page, int perPage, string name = null, LootLockerItemType? itemType = null, bool? consumable = null, LootLockerItemSortField? sort = null, LootLockerSortOrder? order = null, Action onComplete = null, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + EndPointClass endPoint = LootLockerEndPoints.listPlayerInventoryItems; + + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + if (page > 0) + queryParams.Add("page", page); + if (perPage > 0) + queryParams.Add("per_page", perPage); + queryParams.Add("name", name); + if (itemType.HasValue) + queryParams.Add("item_type", itemType.Value.ToString()); + if (consumable.HasValue) + queryParams.Add("consumable", consumable.Value.ToString().ToLower()); + if (sort.HasValue) + queryParams.Add("sort", sort.Value.ToString()); + if (order.HasValue) + queryParams.Add("order", order.Value.ToString()); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// Returns a single inventory item for the current player, including its item template. + /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// + /// The id of the inventory item to fetch. + /// 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 GetPlayerInventoryItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + EndPointClass endPoint = LootLockerEndPoints.getPlayerInventoryItem; + + string getVariable = endPoint.WithPathParameter(inventoryId); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// Deletes an inventory item from the current player's inventory. + /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API (), which deals with + /// assets and asset instances. + /// + /// The id of the inventory item to delete. + /// 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 DeletePlayerInventoryItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + EndPointClass endPoint = LootLockerEndPoints.deletePlayerInventoryItem; + + string getVariable = endPoint.WithPathParameter(inventoryId); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// Consumes one or more items from a stackable inventory item. If is null, a single item is consumed. + /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// + /// The id of the stackable inventory item to consume. + /// (Optional) The number of items to consume. If null, the backend consumes 1. To consume the whole stack, pass the item's current count. + /// 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 ConsumePlayerInventoryItem(string inventoryId, int? count, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + EndPointClass endPoint = LootLockerEndPoints.consumePlayerInventoryItem; + + string getVariable = endPoint.WithPathParameter(inventoryId); + string json = LootLockerJson.SerializeObject(new LootLockerConsumeInventoryItemRequest { count = count }); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// Consumes a single item from a stackable inventory item. + /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// + /// The id of the stackable inventory item to consume. + /// 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 ConsumePlayerInventoryItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) + { + ConsumePlayerInventoryItem(inventoryId, null, onComplete, forPlayerWithUlid); + } + + /// + /// Splits a stackable inventory item into two stacks, moving items into a new stack. + /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// + /// The id of the stackable inventory item to split. + /// The number of items to move into the new stack. + /// 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 SplitPlayerInventoryItemStack(string inventoryId, int count, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + EndPointClass endPoint = LootLockerEndPoints.splitPlayerInventoryItemStack; + + string getVariable = endPoint.WithPathParameter(inventoryId); + string json = LootLockerJson.SerializeObject(new LootLockerSplitInventoryItemStackRequest { count = count }); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// Merges two stacks of the same item into one. + /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// + /// The id of the inventory item to merge from. + /// The id of the inventory item to merge into. + /// 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 MergePlayerInventoryItemStacks(string sourceInventoryId, string targetInventoryId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + EndPointClass endPoint = LootLockerEndPoints.mergePlayerInventoryItemStacks; + + string json = LootLockerJson.SerializeObject(new LootLockerMergeInventoryItemStacksRequest { source_inventory_id = sourceInventoryId, target_inventory_id = targetInventoryId }); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + #endregion + #region AssetInstance progressions /// @ingroup AssetInstanceProgressions diff --git a/Runtime/Game/Requests/ItemRequest.cs b/Runtime/Game/Requests/ItemRequest.cs new file mode 100644 index 000000000..4112d9501 --- /dev/null +++ b/Runtime/Game/Requests/ItemRequest.cs @@ -0,0 +1,322 @@ +using System; +using LootLocker.LootLockerEnums; +using LootLocker.Requests; + +namespace LootLocker.LootLockerEnums +{ + /// + /// The type of an item, determining whether the item instances are stackable or individually tracked. + /// + public enum LootLockerItemType + { + /// Each granted item is a separate, individually tracked instance. + instanced, + /// The item is stored as a single entry with a count that can be incremented or decremented. + stackable + } + + /// + /// The field by which to order a player item list response. + /// + public enum LootLockerItemSortField + { + /// Order by when the item was created. + created_at, + /// Order by when the item was last updated. + updated_at, + /// Order by the source that granted the item. + source + } + + /// + /// The kind of reward that was granted as a result of a behaviour. + /// + public enum LootLockerRewardKind + { + /// The reward kind could not be determined. + unknown, + /// An asset. + asset, + /// Progression points. + progression_points, + /// A progression reset. + progression_reset, + /// A currency. + currency, + /// A group. + group, + /// A reward. + reward, + /// A platform key. + platform_key, + /// A publisher currency. + publisher_currency, + /// Publisher progression points. + publisher_progression_points, + /// Player metadata. + player_metadata, + /// A file. + file, + /// A Discord role. + discord_role, + /// An item template. + item_template + } + + /// + /// The direction in which to order a player item list response. + /// + public enum LootLockerSortOrder + { + /// Order ascending. + ASC, + /// Order descending. + DESC + } +} + +namespace LootLocker.Requests +{ + //================================================== + // Item Template Definitions + //================================================== + + /// + /// An item template defines a type of item that can be granted to players. + /// + public class LootLockerItemTemplate + { + /// The unique id of the item template. + public string id { get; set; } + + /// The name of the item template. + public string name { get; set; } + + /// Indicates whether this item template is limited in how many of it can exist. + public int limited { get; set; } + + /// The type of the item (instanced or stackable). + public LootLockerItemType item_type { get; set; } + + /// True if the item can be consumed by the player. + public bool consumable { get; set; } + + /// True if the item can be deleted by the player. + public bool deletable { get; set; } + + /// The time that this item template was created. + public DateTime created_at { get; set; } + + /// The time that this item template was last updated, or null if it has never been updated. + public DateTime? updated_at { get; set; } + } + + //================================================== + // Player Item Definitions + //================================================== + + /// + /// An item instance held in a player's inventory. + /// + /// + /// This represents an item granted from an item template. It is unrelated to the + /// asset-based inventory API (, GetInventory, + /// ListPlayerInventory), which deals with assets and asset instances. + /// + public class LootLockerInventoryItem + { + /// The unique id of this inventory item. + public string id { get; set; } + + /// The id of the player that owns this inventory item. + public int player_id { get; set; } + + /// The id of the item template this inventory item is based on. + public string item_template_id { get; set; } + + /// The type of the item (instanced or stackable). + public LootLockerItemType item_type { get; set; } + + /// True if the item can be consumed by the player. + public bool consumable { get; set; } + + /// True if the item can be deleted by the player. + public bool deletable { get; set; } + + /// The number of items in this stack (only relevant for stackable items). + public int count { get; set; } + + /// The name of the item template this inventory item is based on. + public string name { get; set; } + + /// The source of this inventory item, or null if not specified. + public string source { get; set; } + + /// The time that this inventory item was created. + public DateTime created_at { get; set; } + + /// The time that this inventory item was last updated, or null if it has never been updated. + public DateTime? updated_at { get; set; } + + /// Arbitrary metadata attached to this inventory item. + public LootLockerMetadataEntry[] metadata { get; set; } + } + + /// + /// An item granted to a player as the result of consuming another item (or via an on-grant behaviour). + /// Matches the go-backend `GrantResult` shape returned in `granted` / `behaviour_results`. + /// + public class LootLockerGrantedItem + { + /// The source id of the granted entry (inventory id, currency id, etc.). + public string source_id { get; set; } + + /// The number of items granted. + public int count { get; set; } + + /// The type of the granted entry. + public LootLockerRewardKind type { get; set; } + + /// The name of the granted entry, if applicable. + public string name { get; set; } + + /// The code of the granted entry, if applicable. + public string code { get; set; } + } + + //================================================== + // Request Definitions + //================================================== + + /// + /// Request to consume a stackable item from the player's inventory. + /// + public class LootLockerConsumeInventoryItemRequest + { + /// + /// The number of items to consume. Defaults to 1 when omitted. To consume an entire stack, + /// pass the item's current count. + /// + public int? count { get; set; } + + public bool ShouldSerializecount() + { + // Omit count entirely when unset so the backend applies its default of 1. This also keeps + // the Newtonsoft and ZeroDep JSON backends consistent, since only ZeroDep skips nulls. + return count.HasValue; + } + } + + /// + /// Request to split a stackable item in the player's inventory into two stacks. + /// + public class LootLockerSplitInventoryItemStackRequest + { + /// The number of items to move into the new stack. + public int count { get; set; } + } + + /// + /// Request to merge two stacks of the same item into one. + /// + public class LootLockerMergeInventoryItemStacksRequest + { + /// The id of the source inventory item to merge from. + public string source_inventory_id { get; set; } + + /// The id of the target inventory item to merge into. + public string target_inventory_id { get; set; } + } + + //================================================== + // Response Definitions + //================================================== + + /// + /// Response containing a paginated list of visible item templates. + /// + public class LootLockerListItemTemplatesResponse : LootLockerResponse + { + /// The list of visible item templates. + public LootLockerItemTemplate[] items { get; set; } + + /// Pagination details for the response. + public LootLockerExtendedPagination pagination { get; set; } + } + + /// + /// Response containing a paginated list of the player's inventory items. + /// + public class LootLockerListPlayerInventoryItemsResponse : LootLockerResponse + { + /// The list of the player's inventory items. + public LootLockerInventoryItem[] items { get; set; } + + /// Pagination details for the response. + public LootLockerExtendedPagination pagination { get; set; } + } + + /// + /// Response containing the player's inventory item, returned at the top level with its template and metadata. + /// + public class LootLockerGetPlayerInventoryItemResponse : LootLockerResponse + { + /// The unique id of this inventory item. + public string id { get; set; } + + /// The id of the player that owns this inventory item. + public int player_id { get; set; } + + /// The id of the item template this inventory item is based on. + public string item_template_id { get; set; } + + /// The type of the item (instanced or stackable). + public LootLockerItemType item_type { get; set; } + + /// True if the item can be consumed by the player. + public bool consumable { get; set; } + + /// True if the item can be deleted by the player. + public bool deletable { get; set; } + + /// The number of items in this stack (only relevant for stackable items). + public int count { get; set; } + + /// The source of this inventory item, or null if not specified. + public string source { get; set; } + + /// The time that this inventory item was created. + public DateTime created_at { get; set; } + + /// The time that this inventory item was last updated, or null if it has never been updated. + public DateTime? updated_at { get; set; } + + /// The item template this inventory item is based on. + public LootLockerItemTemplate template { get; set; } + + /// Arbitrary metadata attached to this inventory item. + public LootLockerMetadataEntry[] metadata { get; set; } + } + + /// + /// Response containing the result of consuming an item, including any items granted as a result. + /// + public class LootLockerConsumeInventoryItemResponse : LootLockerResponse + { + /// True if the item was consumed. + public bool consumed { get; set; } + + /// Any items granted as a result of consuming the item. + public LootLockerGrantedItem[] granted { get; set; } + } + + /// + /// Response containing the id of the newly created stack after splitting an item. + /// + public class LootLockerSplitInventoryItemStackResponse : LootLockerResponse + { + /// The id of the newly created item stack. + public string id { get; set; } + } +} + diff --git a/Runtime/Game/Requests/ItemRequest.cs.meta b/Runtime/Game/Requests/ItemRequest.cs.meta new file mode 100644 index 000000000..0f33bdad8 --- /dev/null +++ b/Runtime/Game/Requests/ItemRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1c184783fa724925b081edf9211f3855 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Game/Requests/MetadataRequests.cs b/Runtime/Game/Requests/MetadataRequests.cs index 896c274c3..4a9e4fdcc 100644 --- a/Runtime/Game/Requests/MetadataRequests.cs +++ b/Runtime/Game/Requests/MetadataRequests.cs @@ -25,7 +25,9 @@ public enum LootLockerMetadataSources player = 5, self = 6, asset = 7, - item = 8, // This is the source for asset instances (player inventory items), while the "asset" source is for the asset in general + item = 8, // This is the source for legacy items, while the "inventory_item" source is for item instances + inventory_item = 9, // This is the source for item instances (player inventory items) + item_template = 10, // This is the source for item templates }; /// diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs index c8f22a1e0..c5ed49b6a 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs @@ -109,6 +109,20 @@ public class LootLockerTestConfigurationEndpoints [Header("LootLocker Admin API Inventory Operations")] public static EndPointClass adminGrantAssetToPlayerInventory = new EndPointClass("/game/#GAMEID#/player/{0}/inventory/grant", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + [Header("LootLocker Admin API Item Template Operations")] + public static EndPointClass createItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass listItemTemplates = new EndPointClass("game/#GAMEID#/templates/v1", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); + public static EndPointClass getItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/{0}", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); + public static EndPointClass updateItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/{0}", LootLockerHTTPMethod.PATCH, LootLockerCallerRole.Admin); + public static EndPointClass deleteItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/{0}", LootLockerHTTPMethod.DELETE, LootLockerCallerRole.Admin); + public static EndPointClass grantItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/grant", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass setItemTemplateAudiences = new EndPointClass("game/#GAMEID#/templates/v1/{0}/audiences", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass adminListPlayerInventoryItems = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); + public static EndPointClass adminConsumePlayerInventoryItem = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}/consume", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass adminDeletePlayerInventoryItem = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}", LootLockerHTTPMethod.DELETE, LootLockerCallerRole.Admin); + public static EndPointClass adminSplitPlayerInventoryItemStack = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}/split", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass adminMergePlayerInventoryItemStacks = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/merge", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + [Header("LootLocker Admin API Metadata Operations")] public static EndPointClass metadataOperations = new EndPointClass("game/#GAMEID#/metadata", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs new file mode 100644 index 000000000..35c01b040 --- /dev/null +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs @@ -0,0 +1,276 @@ +using System; +using LootLocker; +using Random = UnityEngine.Random; + +namespace LootLockerTestConfigurationUtils +{ + /// + /// Admin API helpers for setting up item templates and player inventory items in tests. + /// + /// + /// These helpers deliberately mirror the go-backend admin routes rather than the published + /// ApiDog spec, which is stale for this feature. Notable differences from the spec: + /// the list endpoint returns templates (not items), grant returns id + /// (not inventory_id), consume requires a body and returns a count (not a bool), + /// and the player inventory listing uses template_name and has no deletable. + /// + public static class LootLockerTestItems + { + public static string GetRandomItemTemplateName() + { + string[] colors = { "Green", "Blue", "Red", "Black", "Yellow", "Orange", "Purple", "Indigo", "Clear", "White", "Magenta", "Marine", "Crimson", "Teal" }; + string[] items = { "Rod", "House", "Wand", "Staff", "Car", "Sword", "Shield", "Gun", "Shovel", "Boomstick", "Rifle", "Hut", "Boat", "Bicycle", "Wheelchair" }; + + return colors[Random.Range(0, colors.Length)] + " " + items[Random.Range(0, items.Length)]; + } + + /// + /// Create an item template. Audiences are set inline by the create endpoint, which is + /// required for the template to be visible to the player through the game API. + /// + public static void CreateItemTemplate(string name, string itemType, bool consumable, bool deletable, string[] audiences, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestItemTemplateResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var request = new LootLockerTestCreateItemTemplateRequest + { + name = name, + item_type = itemType, + consumable = consumable, + deletable = deletable, + audiences = audiences + }; + + var endpoint = LootLockerTestConfigurationEndpoints.createItemTemplate; + string json = LootLockerJson.SerializeObject(request); + + LootLockerAdminRequest.Send(endpoint.endPoint, endpoint.httpMethod, json, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + /// + /// Grant an item template to a player. Returns the id of the created inventory item. + /// + public static void GrantItemToPlayer(int playerId, string itemTemplateId, int count, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestGrantItemResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var request = new LootLockerTestGrantItemRequest + { + player_id = playerId, + item_template_id = itemTemplateId, + count = count + }; + + var endpoint = LootLockerTestConfigurationEndpoints.grantItemTemplate; + string json = LootLockerJson.SerializeObject(request); + + LootLockerAdminRequest.Send(endpoint.endPoint, endpoint.httpMethod, json, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + public static void SetItemTemplateAudiences(string itemTemplateId, string[] audiences, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.setItemTemplateAudiences; + string formattedEndpoint = string.Format(endpoint.endPoint, itemTemplateId); + string json = LootLockerJson.SerializeObject(new LootLockerTestSetItemTemplateAudiencesRequest { audiences = audiences }); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete, true); + } + + public static void ListPlayerInventoryItemsAdmin(int playerId, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestAdminListPlayerInventoryItemsResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminListPlayerInventoryItems; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, null, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + /// + /// Consume items through the admin API. The endpoint requires a body carrying the + /// player id; of 0 lets the backend apply its default of 1. + /// + public static void ConsumePlayerInventoryItemAdmin(int playerId, string inventoryId, int count, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestAdminConsumeItemResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminConsumePlayerInventoryItem; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId, inventoryId); + string json = LootLockerJson.SerializeObject(new LootLockerTestAdminConsumeItemRequest { player_id = playerId, count = count }); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + public static void DeletePlayerInventoryItemAdmin(int playerId, string inventoryId, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminDeletePlayerInventoryItem; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId, inventoryId); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, null, onComplete, true); + } + + public static void SplitPlayerInventoryItemStackAdmin(int playerId, string inventoryId, int count, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestSplitItemStackResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminSplitPlayerInventoryItemStack; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId, inventoryId); + string json = LootLockerJson.SerializeObject(new LootLockerTestSplitItemStackRequest { count = count }); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + public static void MergePlayerInventoryItemStacksAdmin(int playerId, string sourceInventoryId, string targetInventoryId, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminMergePlayerInventoryItemStacks; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId); + string json = LootLockerJson.SerializeObject(new LootLockerTestMergeItemStacksRequest + { + source_inventory_id = sourceInventoryId, + target_inventory_id = targetInventoryId + }); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete, true); + } + } + + public class LootLockerTestCreateItemTemplateRequest + { + public string name { get; set; } + public string item_type { get; set; } + public bool consumable { get; set; } + public bool deletable { get; set; } + public string[] audiences { get; set; } + } + + public class LootLockerTestItemTemplateResponse : LootLockerResponse + { + public string id { get; set; } + } + + public class LootLockerTestGrantItemRequest + { + public int player_id { get; set; } + public string item_template_id { get; set; } + public int count { get; set; } + } + + public class LootLockerTestGrantItemResponse : LootLockerResponse + { + public string id { get; set; } + } + + public class LootLockerTestSetItemTemplateAudiencesRequest + { + public string[] audiences { get; set; } + } + + public class LootLockerTestAdminConsumeItemRequest + { + public int player_id { get; set; } + public int count { get; set; } + } + + public class LootLockerTestAdminConsumeItemResponse : LootLockerResponse + { + public int consumed { get; set; } + } + + public class LootLockerTestSplitItemStackRequest + { + public int count { get; set; } + } + + public class LootLockerTestSplitItemStackResponse : LootLockerResponse + { + public string id { get; set; } + } + + public class LootLockerTestMergeItemStacksRequest + { + public string source_inventory_id { get; set; } + public string target_inventory_id { get; set; } + } + + public class LootLockerTestAdminListPlayerInventoryItemsResponse : LootLockerResponse + { + public LootLockerTestAdminPlayerItem[] items { get; set; } + } + + /// + /// An item as returned by the admin player inventory listing. Note that this shape differs + /// from the game API item: the template name is exposed as template_name and there is + /// no deletable field. + /// + public class LootLockerTestAdminPlayerItem + { + public string id { get; set; } + public int player_id { get; set; } + public string item_template_id { get; set; } + public string template_name { get; set; } + public string item_type { get; set; } + public bool consumable { get; set; } + public int count { get; set; } + public string source { get; set; } + public string created_at { get; set; } + public string updated_at { get; set; } + } +} diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs.meta b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs.meta new file mode 100644 index 000000000..3bbd24fd3 --- /dev/null +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 99fdfa0f2eea476a8a97204dcb3005e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs index 329e69284..2c8742877 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs @@ -48,7 +48,9 @@ public enum LootLockerTestMetadataSources player = 5, self = 6, asset = 7, - item = 8, // This is the source for asset instances (player inventory items), while the "asset" source is for the asset in general + item = 8, // This is the source for legacy items, while the "inventory_item" source is for item instances + inventory_item = 9, // This is the source for item instances (player inventory items) + item_template = 10, // This is the source for item templates }; /// diff --git a/Tests/LootLockerTests/PlayMode/ItemTests.cs b/Tests/LootLockerTests/PlayMode/ItemTests.cs new file mode 100644 index 000000000..46ed96bc0 --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/ItemTests.cs @@ -0,0 +1,512 @@ +using System; +using System.Collections; +using System.Linq; +using LootLocker; +using LootLocker.LootLockerEnums; +using LootLocker.Requests; +using LootLockerTestConfigurationUtils; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace LootLockerTests.PlayMode +{ + public class ItemTests + { + private LootLockerTestGame gameUnderTest = null; + private LootLockerConfig configCopy = null; + private static int TestCounter = 0; + private bool SetupFailed = false; + private int sessionPlayerId = 0; + private string createdTemplateId = string.Empty; + private string createdTemplateName = string.Empty; + private string grantedInventoryId = string.Empty; + + private const string PublicAudience = "public"; + + [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"); + } + + bool gameCreationCallCompleted = false; + LootLockerTestGame.CreateGame(testName: this.GetType().Name + TestCounter + " ", onComplete: (success, errorMessage, game) => + { + if (!success) + { + Debug.LogError(errorMessage); + SetupFailed = true; + } + gameUnderTest = game; + gameCreationCallCompleted = true; + }); + yield return new WaitUntil(() => gameCreationCallCompleted); + if (SetupFailed) + { + yield break; + } + + gameUnderTest?.SwitchToStageEnvironment(); + + 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"); + + // Create a stackable, consumable, deletable template with a public audience. The + // audience is required: the game API only lists templates that have an audience row, + // so without it the template would be invisible even to the player it was granted to. + createdTemplateName = LootLockerTestItems.GetRandomItemTemplateName(); + bool createTemplateCallCompleted = false; + LootLockerTestItems.CreateItemTemplate(createdTemplateName, "stackable", true, true, new[] { PublicAudience }, (templateResponse) => + { + if (templateResponse == null || !templateResponse.success) + { + Debug.LogError("Failed to create item template: " + templateResponse?.errorData?.message); + SetupFailed = true; + createTemplateCallCompleted = true; + return; + } + + createdTemplateId = templateResponse.id; + createTemplateCallCompleted = true; + }); + yield return new WaitUntil(() => createTemplateCallCompleted); + if (SetupFailed) + { + yield break; + } + + bool guestLoginCompleted = false; + LootLockerSDKManager.StartGuestSession(Guid.NewGuid().ToString(), response => + { + SetupFailed |= !response.success; + sessionPlayerId = response.player_id; + guestLoginCompleted = true; + }); + yield return new WaitUntil(() => guestLoginCompleted); + if (SetupFailed) + { + yield break; + } + + bool grantCallCompleted = false; + LootLockerTestItems.GrantItemToPlayer(sessionPlayerId, createdTemplateId, 5, (grantResponse) => + { + if (grantResponse == null || !grantResponse.success) + { + Debug.LogError("Failed to grant item to player: " + grantResponse?.errorData?.message); + SetupFailed = true; + grantCallCompleted = true; + return; + } + + grantedInventoryId = grantResponse.id; + grantCallCompleted = true; + }); + yield return new WaitUntil(() => grantCallCompleted); + + 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); + Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); + } + + /// + /// The designated fast test: it covers the whole setup chain (create template with an + /// audience, grant to player) plus the central read path, so it gives early signal + /// without paying for the full suite. + /// + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + [Timeout(360_000)] + public IEnumerator Items_ListItemTemplates_ReturnsCreatedTemplate() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool listCallCompleted = false; + LootLockerListItemTemplatesResponse response = null; + LootLockerSDKManager.ListItemTemplates(1, 100, (r) => + { + response = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + // Then + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "ListItemTemplates call failed"); + Assert.IsNotNull(response.items, "Item templates should not be null"); + Assert.IsTrue(response.items.Any(t => t.id == createdTemplateId), "Expected the created template to be visible to the player"); + + var template = response.items.First(t => t.id == createdTemplateId); + Assert.AreEqual(createdTemplateName, template.name, "Template name should match the created template"); + Assert.AreEqual(LootLockerItemType.stackable, template.item_type, "Template should be stackable"); + Assert.IsTrue(template.consumable, "Template should be consumable"); + Assert.IsTrue(template.deletable, "Template should be deletable"); + Assert.AreNotEqual(default(DateTime), template.created_at, "Template created_at should be populated"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_ListPlayerInventoryItems_ReturnsGrantedItem() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool listCallCompleted = false; + LootLockerListPlayerInventoryItemsResponse response = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => + { + response = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + // Then + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "ListPlayerInventoryItems call failed"); + Assert.IsNotNull(response.items, "Player items should not be null"); + Assert.IsTrue(response.items.Any(i => i.id == grantedInventoryId), "Expected the granted item to be listed"); + + var item = response.items.First(i => i.id == grantedInventoryId); + Assert.AreEqual(5, item.count, "Granted count should be 5"); + Assert.AreEqual(createdTemplateName, item.name, "Item name should be the template name"); + Assert.AreEqual(LootLockerItemType.stackable, item.item_type, "Item should be stackable"); + Assert.IsTrue(item.deletable, "Item should be deletable"); + Assert.IsNotNull(item.source, "Item source should be populated"); + Assert.AreNotEqual(default(DateTime), item.created_at, "Item created_at should be populated"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_ListPlayerInventoryItems_WithFilters_ReturnsFilteredItems() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool listCallCompleted = false; + LootLockerListPlayerInventoryItemsResponse response = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, name: createdTemplateName, itemType: LootLockerItemType.stackable, + consumable: true, sort: LootLockerItemSortField.created_at, order: LootLockerSortOrder.DESC, onComplete: (r) => + { + response = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + // Then + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "Filtered ListPlayerInventoryItems call failed"); + Assert.IsNotNull(response.items, "Player items should not be null"); + Assert.IsTrue(response.items.Any(i => i.id == grantedInventoryId), "Expected the granted item to match the filters"); + Assert.IsTrue(response.items.All(i => i.item_type == LootLockerItemType.stackable), "All returned items should be stackable"); + Assert.IsTrue(response.items.All(i => i.consumable), "All returned items should be consumable"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_GetPlayerInventoryItem_ReturnsItemWithTemplate() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool getCallCompleted = false; + LootLockerGetPlayerInventoryItemResponse response = null; + LootLockerSDKManager.GetPlayerInventoryItem(grantedInventoryId, (r) => + { + response = r; + getCallCompleted = true; + }); + yield return new WaitUntil(() => getCallCompleted); + + // Then + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "GetPlayerInventoryItem call failed"); + Assert.AreEqual(grantedInventoryId, response.id, "Returned item id should match the requested id"); + Assert.AreEqual(5, response.count, "Item count should be 5"); + Assert.AreEqual(LootLockerItemType.stackable, response.item_type, "Item should be stackable"); + Assert.IsTrue(response.deletable, "Item should be deletable"); + Assert.IsNotNull(response.template, "Item template should be populated on the single item response"); + Assert.AreEqual(createdTemplateId, response.template.id, "Template id should match the created template"); + Assert.AreEqual(createdTemplateName, response.template.name, "Template name should match the created template"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_ConsumePlayerInventoryItem_PartialStack_DecrementsCount() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool consumeCallCompleted = false; + LootLockerConsumeInventoryItemResponse consumeResponse = null; + LootLockerSDKManager.ConsumePlayerInventoryItem(grantedInventoryId, 2, (r) => + { + consumeResponse = r; + consumeCallCompleted = true; + }); + yield return new WaitUntil(() => consumeCallCompleted); + + // Then + Assert.IsTrue(consumeResponse.success, consumeResponse.errorData?.ToString() ?? "ConsumePlayerInventoryItem call failed"); + Assert.IsTrue(consumeResponse.consumed, "Item should report as consumed"); + + bool getCallCompleted = false; + LootLockerGetPlayerInventoryItemResponse getResponse = null; + LootLockerSDKManager.GetPlayerInventoryItem(grantedInventoryId, (r) => + { + getResponse = r; + getCallCompleted = true; + }); + yield return new WaitUntil(() => getCallCompleted); + + Assert.IsTrue(getResponse.success, getResponse.errorData?.ToString() ?? "GetPlayerInventoryItem after consume failed"); + Assert.AreEqual(3, getResponse.count, "Consuming 2 of 5 should leave 3"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_ConsumePlayerInventoryItem_WholeStack_RemovesItem() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When - passing the full count consumes the entire row + bool consumeCallCompleted = false; + LootLockerConsumeInventoryItemResponse consumeResponse = null; + LootLockerSDKManager.ConsumePlayerInventoryItem(grantedInventoryId, 5, (r) => + { + consumeResponse = r; + consumeCallCompleted = true; + }); + yield return new WaitUntil(() => consumeCallCompleted); + + // Then + Assert.IsTrue(consumeResponse.success, consumeResponse.errorData?.ToString() ?? "ConsumePlayerInventoryItem call failed"); + Assert.IsTrue(consumeResponse.consumed, "Item should report as consumed"); + + bool listCallCompleted = false; + LootLockerListPlayerInventoryItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => + { + listResponse = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerInventoryItems after consume failed"); + Assert.IsFalse(listResponse.items.Any(i => i.id == grantedInventoryId), "Fully consumed item should no longer be listed"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_SplitPlayerInventoryItemStack_CreatesSecondStack() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool splitCallCompleted = false; + LootLockerSplitInventoryItemStackResponse splitResponse = null; + LootLockerSDKManager.SplitPlayerInventoryItemStack(grantedInventoryId, 2, (r) => + { + splitResponse = r; + splitCallCompleted = true; + }); + yield return new WaitUntil(() => splitCallCompleted); + + // Then + Assert.IsTrue(splitResponse.success, splitResponse.errorData?.ToString() ?? "SplitPlayerInventoryItemStack call failed"); + Assert.IsNotNull(splitResponse.id, "Split should return the id of the new stack"); + Assert.AreNotEqual(grantedInventoryId, splitResponse.id, "The new stack should have a different id"); + + bool listCallCompleted = false; + LootLockerListPlayerInventoryItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => + { + listResponse = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerInventoryItems after split failed"); + var original = listResponse.items.FirstOrDefault(i => i.id == grantedInventoryId); + var split = listResponse.items.FirstOrDefault(i => i.id == splitResponse.id); + Assert.IsNotNull(original, "Original stack should still be listed"); + Assert.IsNotNull(split, "New stack should be listed"); + Assert.AreEqual(3, original.count, "Original stack should be left with 3"); + Assert.AreEqual(2, split.count, "New stack should hold 2"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_MergePlayerInventoryItemStacks_CombinesCounts() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // Given - split off a second stack to merge back + bool splitCallCompleted = false; + LootLockerSplitInventoryItemStackResponse splitResponse = null; + LootLockerSDKManager.SplitPlayerInventoryItemStack(grantedInventoryId, 2, (r) => + { + splitResponse = r; + splitCallCompleted = true; + }); + yield return new WaitUntil(() => splitCallCompleted); + Assert.IsTrue(splitResponse.success, splitResponse.errorData?.ToString() ?? "SplitPlayerInventoryItemStack call failed"); + + // When - merge the new stack back into the original + bool mergeCallCompleted = false; + LootLockerResponse mergeResponse = null; + LootLockerSDKManager.MergePlayerInventoryItemStacks(splitResponse.id, grantedInventoryId, (r) => + { + mergeResponse = r; + mergeCallCompleted = true; + }); + yield return new WaitUntil(() => mergeCallCompleted); + + // Then + Assert.IsTrue(mergeResponse.success, mergeResponse.errorData?.ToString() ?? "MergePlayerInventoryItemStacks call failed"); + + bool listCallCompleted = false; + LootLockerListPlayerInventoryItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => + { + listResponse = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerInventoryItems after merge failed"); + var target = listResponse.items.FirstOrDefault(i => i.id == grantedInventoryId); + Assert.IsNotNull(target, "Target stack should still be listed"); + Assert.AreEqual(5, target.count, "Merged stack should hold the combined count of 5"); + Assert.IsFalse(listResponse.items.Any(i => i.id == splitResponse.id), "Source stack should be gone after merging"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_DeletePlayerInventoryItem_RemovesItem() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool deleteCallCompleted = false; + LootLockerResponse deleteResponse = null; + LootLockerSDKManager.DeletePlayerInventoryItem(grantedInventoryId, (r) => + { + deleteResponse = r; + deleteCallCompleted = true; + }); + yield return new WaitUntil(() => deleteCallCompleted); + + // Then + Assert.IsTrue(deleteResponse.success, deleteResponse.errorData?.ToString() ?? "DeletePlayerInventoryItem call failed"); + + bool listCallCompleted = false; + LootLockerListPlayerInventoryItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => + { + listResponse = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerInventoryItems after delete failed"); + Assert.IsFalse(listResponse.items.Any(i => i.id == grantedInventoryId), "Deleted item should no longer be listed"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_DeletePlayerInventoryItem_NotDeletable_ReturnsForbidden() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // Given - a second template that is explicitly not deletable + bool createTemplateCallCompleted = false; + string nonDeletableTemplateId = string.Empty; + LootLockerTestItems.CreateItemTemplate(LootLockerTestItems.GetRandomItemTemplateName(), "stackable", false, false, new[] { PublicAudience }, (templateResponse) => + { + if (templateResponse == null || !templateResponse.success) + { + Debug.LogError("Failed to create non-deletable item template: " + templateResponse?.errorData?.message); + createTemplateCallCompleted = true; + return; + } + + nonDeletableTemplateId = templateResponse.id; + createTemplateCallCompleted = true; + }); + yield return new WaitUntil(() => createTemplateCallCompleted); + Assert.IsNotEmpty(nonDeletableTemplateId, "Failed to create the non-deletable template"); + + bool grantCallCompleted = false; + string nonDeletableInventoryId = string.Empty; + LootLockerTestItems.GrantItemToPlayer(sessionPlayerId, nonDeletableTemplateId, 1, (grantResponse) => + { + if (grantResponse == null || !grantResponse.success) + { + Debug.LogError("Failed to grant non-deletable item: " + grantResponse?.errorData?.message); + grantCallCompleted = true; + return; + } + + nonDeletableInventoryId = grantResponse.id; + grantCallCompleted = true; + }); + yield return new WaitUntil(() => grantCallCompleted); + Assert.IsNotEmpty(nonDeletableInventoryId, "Failed to grant the non-deletable item"); + + // When + bool deleteCallCompleted = false; + LootLockerResponse deleteResponse = null; + LootLockerSDKManager.DeletePlayerInventoryItem(nonDeletableInventoryId, (r) => + { + deleteResponse = r; + deleteCallCompleted = true; + }); + yield return new WaitUntil(() => deleteCallCompleted); + + // Then + Assert.IsFalse(deleteResponse.success, "Deleting a non-deletable item should fail"); + Assert.AreEqual(403, deleteResponse.statusCode, "Deleting a non-deletable item should return 403 Forbidden"); + } + } +} diff --git a/Tests/LootLockerTests/PlayMode/ItemTests.cs.meta b/Tests/LootLockerTests/PlayMode/ItemTests.cs.meta new file mode 100644 index 000000000..354a1e1fb --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/ItemTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7fed9865e8244b08bd29cda5c063248a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Tests/LootLockerTests/PlayMode/JsonTests.cs b/Tests/LootLockerTests/PlayMode/JsonTests.cs index f78fe1a6d..f9dbc1697 100644 --- a/Tests/LootLockerTests/PlayMode/JsonTests.cs +++ b/Tests/LootLockerTests/PlayMode/JsonTests.cs @@ -1,4 +1,5 @@ -using LootLocker; +using LootLocker; +using LootLocker.LootLockerEnums; using LootLocker.Requests; using System; using System.Collections; @@ -315,6 +316,186 @@ public void Json_DeserializationCaseConversion_CaseIsConvertedToSnake() #endif } + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_SerializingConsumeItemRequestWithoutCount_OmitsCount() + { + // Given + var request = new LootLockerConsumeInventoryItemRequest(); + + // When + string serializedJson = LootLockerJson.SerializeObject(request); + + // Then + Assert.IsNotNull(serializedJson, "Not serialized, is null"); + Assert.IsFalse(serializedJson.Contains("count"), + "Not serialized correctly, contains count even though it was never set"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_SerializingConsumeItemRequestWithCount_IncludesCount() + { + // Given + var request = new LootLockerConsumeInventoryItemRequest { count = 3 }; + + // When + string serializedJson = LootLockerJson.SerializeObject(request); + + // Then + Assert.IsNotNull(serializedJson, "Not serialized, is null"); + Assert.IsTrue(serializedJson.Contains("count"), + "Not serialized correctly, does not contain count"); + Assert.IsTrue(serializedJson.Contains("3"), + "Not serialized correctly, does not contain the count value"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_SerializingSplitItemStackRequest_IncludesCount() + { + // Given + var request = new LootLockerSplitInventoryItemStackRequest { count = 5 }; + + // When + string serializedJson = LootLockerJson.SerializeObject(request); + + // Then + Assert.IsNotNull(serializedJson, "Not serialized, is null"); + Assert.IsTrue(serializedJson.Contains("count"), + "Not serialized correctly, does not contain count"); + Assert.IsTrue(serializedJson.Contains("5"), + "Not serialized correctly, does not contain the count value"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_SerializingMergeItemStacksRequest_IncludesBothInventoryIds() + { + // Given + var request = new LootLockerMergeInventoryItemStacksRequest + { + source_inventory_id = "01HZZZZZZZZZZZZZZZZZZZZZZZ", + target_inventory_id = "01HYYYYYYYYYYYYYYYYYYYYYYY" + }; + + // When + string serializedJson = LootLockerJson.SerializeObject(request); + + // Then + Assert.IsNotNull(serializedJson, "Not serialized, is null"); + Assert.IsTrue(serializedJson.Contains("source_inventory_id"), + "Not serialized correctly, does not contain source_inventory_id"); + Assert.IsTrue(serializedJson.Contains("target_inventory_id"), + "Not serialized correctly, does not contain target_inventory_id"); + Assert.IsTrue(serializedJson.Contains("01HZZZZZZZZZZZZZZZZZZZZZZZ"), + "Not serialized correctly, does not contain the source inventory id value"); + Assert.IsTrue(serializedJson.Contains("01HYYYYYYYYYYYYYYYYYYYYYYY"), + "Not serialized correctly, does not contain the target inventory id value"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_DeserializingListPlayerInventoryItemsResponse_Succeeds() + { + // Given + const string listPlayerItemsResponse = + "{\"success\":true,\"items\":[{\"id\":\"01HZZZZZZZZZZZZZZZZZZZZZZZ\",\"player_id\":3,\"item_template_id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"item_type\":\"stackable\",\"consumable\":true,\"count\":7,\"source\":\"grant\",\"name\":\"Health Potion\",\"deletable\":true,\"created_at\":\"2024-01-01T00:00:00Z\"}],\"pagination\":{\"total\":1,\"offset\":0,\"per_page\":25,\"last_page\":1,\"current_page\":1,\"next_page\":null,\"prev_page\":null}}"; + + // When + var deserialized = LootLockerJson.DeserializeObject(listPlayerItemsResponse); + + // Then + Assert.IsNotNull(deserialized, "Not deserialized, is null"); + Assert.IsNotNull(deserialized.items, "Not deserialized, items is null"); + Assert.AreEqual(1, deserialized.items.Length, "Not deserialized, wrong number of items"); + Assert.AreEqual("01HZZZZZZZZZZZZZZZZZZZZZZZ", deserialized.items[0].id, "Wrong item id"); + Assert.AreEqual(3, deserialized.items[0].player_id, "Wrong player id"); + Assert.AreEqual("01HAAAAAAAAAAAAAAAAAAAAAAA", deserialized.items[0].item_template_id, "Wrong item template id"); + Assert.AreEqual(LootLockerItemType.stackable, deserialized.items[0].item_type, "Wrong item type"); + Assert.IsTrue(deserialized.items[0].consumable, "Wrong consumable value"); + Assert.AreEqual(7, deserialized.items[0].count, "Wrong count"); + Assert.AreEqual("Health Potion", deserialized.items[0].name, "Wrong name"); + Assert.IsTrue(deserialized.items[0].deletable, "Wrong deletable value"); + Assert.AreNotEqual(default(DateTime), deserialized.items[0].created_at, "Wrong created_at value"); + Assert.IsNull(deserialized.items[0].updated_at, "Wrong updated_at value"); + Assert.IsNotNull(deserialized.pagination, "Not deserialized, pagination is null"); + Assert.AreEqual(1, deserialized.pagination.total, "Wrong pagination total"); + Assert.AreEqual(25, deserialized.pagination.per_page, "Wrong pagination per_page"); + Assert.IsNull(deserialized.pagination.next_page, "Wrong pagination next_page"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_DeserializingGetPlayerInventoryItemResponseWithTemplateAndMetadata_Succeeds() + { + // Given + const string getPlayerItemResponse = + "{\"success\":true,\"id\":\"01HZZZZZZZZZZZZZZZZZZZZZZZ\",\"player_id\":3,\"item_template_id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"item_type\":\"instanced\",\"consumable\":false,\"count\":1,\"source\":\"grant\",\"deletable\":true,\"created_at\":\"2024-01-01T00:00:00Z\",\"template\":{\"id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"name\":\"Sword\",\"game_id\":42,\"limited\":0,\"item_type\":\"instanced\",\"consumable\":false,\"deletable\":true},\"metadata\":[{\"key\":\"damage\",\"value\":12,\"type\":\"number\",\"access\":[\"game_api.read\"],\"tags\":[\"combat\"]}]}"; + + // When + var deserialized = LootLockerJson.DeserializeObject(getPlayerItemResponse); + + // Then + Assert.IsNotNull(deserialized, "Not deserialized, is null"); + Assert.AreEqual("01HZZZZZZZZZZZZZZZZZZZZZZZ", deserialized.id, "Wrong item id"); + Assert.AreEqual(LootLockerItemType.instanced, deserialized.item_type, "Wrong item type"); + Assert.IsTrue(deserialized.deletable, "Wrong deletable value"); + Assert.AreNotEqual(default(DateTime), deserialized.created_at, "Wrong created_at value"); + Assert.IsNotNull(deserialized.template, "Not deserialized, template is null"); + Assert.AreEqual("Sword", deserialized.template.name, "Wrong template name"); + Assert.AreEqual(LootLockerItemType.instanced, deserialized.template.item_type, "Wrong template item type"); + Assert.IsNotNull(deserialized.metadata, "Not deserialized, metadata is null"); + Assert.AreEqual(1, deserialized.metadata.Length, "Not deserialized, wrong number of metadata entries"); + Assert.AreEqual("damage", deserialized.metadata[0].key, "Wrong metadata key"); + Assert.AreEqual(LootLockerMetadataTypes.Number, deserialized.metadata[0].type, "Wrong metadata type"); + Assert.IsNotNull(deserialized.metadata[0].access, "Not deserialized, metadata access is null"); + Assert.AreEqual("game_api.read", deserialized.metadata[0].access[0], "Wrong metadata access"); + Assert.IsNotNull(deserialized.metadata[0].tags, "Not deserialized, metadata tags is null"); + Assert.AreEqual("combat", deserialized.metadata[0].tags[0], "Wrong metadata tag"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_DeserializingConsumeItemResponseWithGrantedItems_Succeeds() + { + // Given + const string consumeItemResponse = + "{\"success\":true,\"consumed\":true,\"granted\":[{\"source_id\":\"01HBBBBBBBBBBBBBBBBBBBBBBB\",\"count\":2,\"type\":\"currency\",\"name\":\"Gold\",\"code\":\"gold\"}]}"; + + // When + var deserialized = LootLockerJson.DeserializeObject(consumeItemResponse); + + // Then + Assert.IsNotNull(deserialized, "Not deserialized, is null"); + Assert.IsTrue(deserialized.consumed, "Wrong consumed value"); + Assert.IsNotNull(deserialized.granted, "Not deserialized, granted is null"); + Assert.AreEqual(1, deserialized.granted.Length, "Not deserialized, wrong number of granted items"); + Assert.AreEqual("01HBBBBBBBBBBBBBBBBBBBBBBB", deserialized.granted[0].source_id, "Wrong granted source id"); + Assert.AreEqual(2, deserialized.granted[0].count, "Wrong granted count"); + Assert.AreEqual(LootLockerRewardKind.currency, deserialized.granted[0].type, "Wrong granted type"); + Assert.AreEqual("Gold", deserialized.granted[0].name, "Wrong granted name"); + Assert.AreEqual("gold", deserialized.granted[0].code, "Wrong granted code"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_DeserializingListItemTemplatesResponse_Succeeds() + { + // Given + const string listItemTemplatesResponse = + "{\"success\":true,\"items\":[{\"id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"name\":\"Sword\",\"game_id\":42,\"limited\":10,\"item_type\":\"instanced\",\"consumable\":false,\"deletable\":true,\"created_at\":\"2024-01-01T00:00:00Z\",\"updated_at\":\"2024-01-02T00:00:00Z\"}],\"pagination\":{\"total\":1,\"offset\":0,\"per_page\":25,\"last_page\":1,\"current_page\":1,\"next_page\":null,\"prev_page\":null}}"; + + // When + var deserialized = LootLockerJson.DeserializeObject(listItemTemplatesResponse); + + // Then + Assert.IsNotNull(deserialized, "Not deserialized, is null"); + Assert.IsNotNull(deserialized.items, "Not deserialized, items is null"); + Assert.AreEqual(1, deserialized.items.Length, "Not deserialized, wrong number of item templates"); + Assert.AreEqual("01HAAAAAAAAAAAAAAAAAAAAAAA", deserialized.items[0].id, "Wrong item template id"); + Assert.AreEqual("Sword", deserialized.items[0].name, "Wrong item template name"); + Assert.AreEqual(LootLockerItemType.instanced, deserialized.items[0].item_type, "Wrong item template item type"); + Assert.AreEqual(10, deserialized.items[0].limited, "Wrong item template limited value"); + Assert.IsTrue(deserialized.items[0].deletable, "Wrong item template deletable value"); + Assert.AreNotEqual(default(DateTime), deserialized.items[0].created_at, "Wrong item template created_at value"); + Assert.IsNotNull(deserialized.items[0].updated_at, "Wrong item template updated_at value"); + Assert.IsNotNull(deserialized.pagination, "Not deserialized, pagination is null"); + Assert.AreEqual(1, deserialized.pagination.total, "Wrong pagination total"); + } + #if !LOOTLOCKER_USE_NEWTONSOFTJSON [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_SimpleTypeSerialization_Succeeds() @@ -450,6 +631,7 @@ public void Json_SerializingAndDesierializingEnumArrays_Works() Assert.IsNotEmpty(deserialized.enumArray); Assert.AreNotEqual(defaultEnumValue, deserialized.enumArray[0]); } + } class CustomOptions : JsonOptions