diff --git a/src/Abstractions/NexusMods.Abstractions.Collections/Json/ModSource.cs b/src/Abstractions/NexusMods.Abstractions.Collections/Json/ModSource.cs index b1fc29669f..886f8e7c04 100644 --- a/src/Abstractions/NexusMods.Abstractions.Collections/Json/ModSource.cs +++ b/src/Abstractions/NexusMods.Abstractions.Collections/Json/ModSource.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.Paths; namespace NexusMods.Abstractions.Collections.Json; diff --git a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsFileMetadata.cs b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsFileMetadata.cs index 9abb540761..82c4c04bdd 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsFileMetadata.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsFileMetadata.cs @@ -1,6 +1,7 @@ using JetBrains.Annotations; using NexusMods.Abstractions.MnemonicDB.Attributes; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; diff --git a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsModPageMetadata.cs b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsModPageMetadata.cs index f92f049f77..79331e3585 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsModPageMetadata.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsModPageMetadata.cs @@ -2,6 +2,7 @@ using NexusMods.Abstractions.MnemonicDB.Attributes; using NexusMods.Abstractions.NexusWebApi.Types; using NexusMods.Abstractions.Resources.DB; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.Abstractions.Telemetry; using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/GameInfo.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/GameInfo.cs index 3bae052e78..4c8f9c8c17 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/GameInfo.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/GameInfo.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using NexusMods.Abstractions.NexusWebApi.DTOs.Interfaces; -using GameId = NexusMods.Abstractions.NexusWebApi.Types.GameId; +using GameId = NexusMods.Abstractions.NexusWebApi.Types.V2.GameId; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable InconsistentNaming @@ -27,7 +27,7 @@ public class GameInfo : IJsonArraySerializable /// This field is for deserialization only. /// [JsonPropertyName("id")] - public int _Id { get; set; } + public uint _Id { get; set; } /// /// Returns the ID as typed ValueObject . diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModFile.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModFile.cs index 2faa6b7488..1167260354 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModFile.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModFile.cs @@ -2,7 +2,8 @@ using System.Text.Json.Serialization.Metadata; using NexusMods.Abstractions.NexusWebApi.DTOs.Interfaces; using NexusMods.Abstractions.NexusWebApi.Types; -using FileId = NexusMods.Abstractions.NexusWebApi.Types.FileId; +using NexusMods.Abstractions.NexusWebApi.Types.V2; +using FileId = NexusMods.Abstractions.NexusWebApi.Types.V2.FileId; // 👇 Suppress uninitialised variables. Currently Nexus has mostly read-only API and we expect server to return the data. #pragma warning disable CS8618 @@ -47,7 +48,7 @@ public class ModFile : IJsonSerializable /// This ID is unique within the context of the game. /// i.e. This ID might be used for another mod if you search for mods for another game. /// - public FileId FileId => FileId.From(_FileId); + public FileId FileId => FileId.From((uint)_FileId); /// /// Name (title) of the mod file as seen on the `Files` section of the mod page. diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModUpdate.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModUpdate.cs index bb71b4a136..89c9b9ff1f 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModUpdate.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/DTOs/ModUpdate.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using NexusMods.Abstractions.NexusWebApi.DTOs.Interfaces; -using ModId = NexusMods.Abstractions.NexusWebApi.Types.ModId; +using ModId = NexusMods.Abstractions.NexusWebApi.Types.V2.ModId; // 👇 Suppress uninitialised variables. Currently Nexus has mostly read-only API and we expect server to return the data. #pragma warning disable CS8618 @@ -25,7 +25,7 @@ public class ModUpdate : IJsonArraySerializable /// /// An individual mod ID that is unique for this game. /// - public ModId ModId => ModId.From(_ModId); + public ModId ModId => ModId.From((uint)_ModId); /// /// The last time a file on the mod page was updated. diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/INexusApiClient.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/INexusApiClient.cs index bed70e56ae..b922853450 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/INexusApiClient.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/INexusApiClient.cs @@ -1,6 +1,8 @@ using NexusMods.Abstractions.NexusWebApi.DTOs; using NexusMods.Abstractions.NexusWebApi.DTOs.OAuth; using NexusMods.Abstractions.NexusWebApi.Types; +using FileId = NexusMods.Abstractions.NexusWebApi.Types.V2.FileId; +using ModId = NexusMods.Abstractions.NexusWebApi.Types.V2.ModId; namespace NexusMods.Abstractions.NexusWebApi; diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/NexusMods.Abstractions.NexusWebApi.csproj b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/NexusMods.Abstractions.NexusWebApi.csproj index 3823713dcc..2d62ced743 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/NexusMods.Abstractions.NexusWebApi.csproj +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/NexusMods.Abstractions.NexusWebApi.csproj @@ -12,4 +12,8 @@ + + + + diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/NexusModsArchiveMetadata.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/NexusModsArchiveMetadata.cs index f5bf9d569c..c2f44012c7 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/NexusModsArchiveMetadata.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/NexusModsArchiveMetadata.cs @@ -1,4 +1,5 @@ using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/FileId.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/FileId.cs deleted file mode 100644 index 580865dcf2..0000000000 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/FileId.cs +++ /dev/null @@ -1,33 +0,0 @@ -using NexusMods.MnemonicDB.Abstractions; -using NexusMods.MnemonicDB.Abstractions.Attributes; -using NexusMods.MnemonicDB.Abstractions.ElementComparers; -using TransparentValueObjects; - -namespace NexusMods.Abstractions.NexusWebApi.Types; - -/// -/// Unique ID for a game file hosted on a mod page. -/// -/// This ID is unique within the context of the game. -/// i.e. This ID might be used for another mod if you search for mods for another game. -/// -[ValueObject] -public readonly partial struct FileId : IAugmentWith, IAugmentWith -{ - /// - public static FileId DefaultValue => From(default); -} - - -/// -/// File ID attribute, for NexusMods API file IDs. -/// -public class FileIdAttribute(string ns, string name) : - ScalarAttribute(ValueTags.UInt64, ns, name) -{ - /// - protected override ulong ToLowLevel(FileId value) => value.Value; - - /// - protected override FileId FromLowLevel(ulong value, ValueTags tags, AttributeResolver resolver) => FileId.From(value); -} diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/NXMModUrl.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/NXMModUrl.cs index eaeeff56b0..c445738df7 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/NXMModUrl.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/NXMModUrl.cs @@ -9,12 +9,12 @@ public class NXMModUrl : NXMUrl /// /// id of the mod page /// - public ModId ModId { get; set; } + public V2.ModId ModId { get; set; } /// /// id of the file (within that game domain) /// - public FileId FileId { get; set; } + public V2.FileId FileId { get; set; } /// /// game domain (name of the game within the Nexus Mods page) @@ -36,8 +36,8 @@ public NXMModUrl(Uri uri) Game = uri.Host; try { - ModId = ModId.From(ulong.Parse(uri.Segments[2].TrimEnd('/'))); - FileId = FileId.From(ulong.Parse(uri.Segments[4].TrimEnd('/'))); + ModId = V2.ModId.From(uint.Parse(uri.Segments[2].TrimEnd('/'))); + FileId = V2.FileId.From(uint.Parse(uri.Segments[4].TrimEnd('/'))); } catch (FormatException) { diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/FileId.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/FileId.cs new file mode 100644 index 0000000000..088ff88517 --- /dev/null +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/FileId.cs @@ -0,0 +1,29 @@ +using NexusMods.MnemonicDB.Abstractions; +using NexusMods.MnemonicDB.Abstractions.Attributes; +using NexusMods.MnemonicDB.Abstractions.ElementComparers; +using TransparentValueObjects; +namespace NexusMods.Abstractions.NexusWebApi.Types.V2; + +/// +/// Unique ID for a mod file associated with a game (). +/// Querying mod pages returns items of this type. +/// +[ValueObject] // Matches backend. Do not change. +public readonly partial struct FileId : IAugmentWith, IAugmentWith +{ + /// + public static FileId DefaultValue => From(default(uint)); +} + +/// +/// File ID attribute, for NexusMods API file IDs. +/// +public class FileIdAttribute(string ns, string name) : + ScalarAttribute(ValueTags.UInt32, ns, name) +{ + /// + protected override uint ToLowLevel(FileId value) => value.Value; + + /// + protected override FileId FromLowLevel(uint value, ValueTags tags, AttributeResolver resolver) => FileId.From(value); +} diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/GameId.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/GameId.cs similarity index 56% rename from src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/GameId.cs rename to src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/GameId.cs index e33aa6f99e..a798ab4adb 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/GameId.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/GameId.cs @@ -1,13 +1,12 @@ using TransparentValueObjects; - -namespace NexusMods.Abstractions.NexusWebApi.Types; +namespace NexusMods.Abstractions.NexusWebApi.Types.V2; /// /// Unique identifier for an individual game hosted on Nexus. /// -[ValueObject] +[ValueObject] // Matches backend. Do not change. public readonly partial struct GameId : IAugmentWith { /// - public static GameId DefaultValue => From(default); + public static GameId DefaultValue => From(default(uint)); } diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/ModId.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/ModId.cs similarity index 54% rename from src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/ModId.cs rename to src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/ModId.cs index f4fa915b95..4473ff0bca 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/ModId.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/ModId.cs @@ -2,18 +2,17 @@ using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.ElementComparers; using TransparentValueObjects; - -namespace NexusMods.Abstractions.NexusWebApi.Types; +namespace NexusMods.Abstractions.NexusWebApi.Types.V2; /// -/// An individual mod ID. Unique per game. +/// An individual mod ID. Unique per . /// i.e. Each game has its own set of IDs and starts with 0. /// -[ValueObject] +[ValueObject] // Matches backend. Do not change. public readonly partial struct ModId : IAugmentWith, IAugmentWith { /// - public static ModId DefaultValue => From(default); + public static ModId DefaultValue => From(default(uint)); } @@ -21,11 +20,11 @@ namespace NexusMods.Abstractions.NexusWebApi.Types; /// Mod ID attribute, for NexusMods API mod IDs. /// public class ModIdAttribute(string ns, string name) - : ScalarAttribute(ValueTags.UInt64, ns, name) + : ScalarAttribute(ValueTags.UInt32, ns, name) { /// - protected override ulong ToLowLevel(ModId value) => value.Value; + protected override uint ToLowLevel(ModId value) => value.Value; /// - protected override ModId FromLowLevel(ulong value, ValueTags tags, AttributeResolver resolver) => ModId.From(value); + protected override ModId FromLowLevel(uint value, ValueTags tags, AttributeResolver resolver) => ModId.From(value); } diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/Uid/UidForFile.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/Uid/UidForFile.cs new file mode 100644 index 0000000000..f644dab2b5 --- /dev/null +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/Uid/UidForFile.cs @@ -0,0 +1,46 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +namespace NexusMods.Abstractions.NexusWebApi.Types.V2.Uid; + +/// +/// This represents a unique ID of an individual file as stored on Nexus Mods. +/// +/// This is a composite key of and , where +/// the upper 4 bytes represent the and the lower 4 bytes represent +/// the . +/// +/// This is consistent with how the Nexus Mods backend produces the UID and is not +/// expected to change. +/// +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct UidForFile +{ + /// + /// Unique identifier for the file, within the specific . + /// + public FileId FileId; + + /// + /// Unique identifier for the game. + /// + public GameId GameId; + + /// + /// Decodes a Nexus Mods API result which contains an 'uid' field into a . + /// + /// The 'uid' field of a GraphQL API query. This should be an 8 byte number represented as a string. + /// + /// This throws if is not a valid number. + /// + public static UidForFile FromV2Api(string uid) => FromUlong(ulong.Parse(uid)); + + /// + /// Reinterprets the current as a single . + /// + public ulong AsUlong => Unsafe.As(ref this); + + /// + /// Reinterprets a given into a . + /// + public static UidForFile FromUlong(ulong value) => Unsafe.As(ref value); +} diff --git a/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/Uid/UidForMod.cs b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/Uid/UidForMod.cs new file mode 100644 index 0000000000..1e5f8987d7 --- /dev/null +++ b/src/Abstractions/NexusMods.Abstractions.NexusWebApi/Types/V2/Uid/UidForMod.cs @@ -0,0 +1,49 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +namespace NexusMods.Abstractions.NexusWebApi.Types.V2.Uid; + +/// +/// This represents a unique ID of an individual mod page as stored on Nexus Mods. +/// +/// This is a composite key of and , where +/// the upper 4 bytes represent the and the lower 4 bytes represent +/// the . Values are stored in little endian byte order. +/// +/// When transferred over the wire via the API, the resulting `ulong` is converted into +/// a string. +/// +/// This is consistent with how the Nexus Mods backend produces the UID and is not +/// expected to change. +/// +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct UidForMod +{ + /// + /// Unique identifier for the mod, within the specific . + /// + public ModId ModId; + + /// + /// Unique identifier for the game. + /// + public GameId GameId; + + /// + /// Decodes a Nexus Mods API result which contains an 'uid' field into a . + /// + /// The 'uid' field of a GraphQL API query. This should be an 8 byte number represented as a string. + /// + /// This throws if is not a valid number. + /// + public static UidForMod FromV2Api(string uid) => FromUlong(ulong.Parse(uid)); + + /// + /// Reinterprets the current as a single . + /// + public ulong AsUlong => Unsafe.As(ref this); + + /// + /// Reinterprets a given into a . + /// + public static UidForMod FromUlong(ulong value) => Unsafe.As(ref value); +} diff --git a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Emitters/Pattern.cs b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Emitters/Pattern.cs index efc561b535..5651746ce0 100644 --- a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Emitters/Pattern.cs +++ b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Emitters/Pattern.cs @@ -2,6 +2,7 @@ using DynamicData.Kernel; using NexusMods.Abstractions.GameLocators; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.Paths; namespace NexusMods.Games.RedEngine.Cyberpunk2077.Emitters; diff --git a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Emitters/PatternDefinitions.cs b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Emitters/PatternDefinitions.cs index 9937083dfb..37429ebde1 100644 --- a/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Emitters/PatternDefinitions.cs +++ b/src/Games/NexusMods.Games.RedEngine/Cyberpunk2077/Emitters/PatternDefinitions.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using NexusMods.Abstractions.GameLocators; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.Paths; namespace NexusMods.Games.RedEngine.Cyberpunk2077.Emitters; diff --git a/src/Games/NexusMods.Games.TestHarness/Verbs/StressTest.cs b/src/Games/NexusMods.Games.TestHarness/Verbs/StressTest.cs index 693bd3b37d..05ceba0625 100644 --- a/src/Games/NexusMods.Games.TestHarness/Verbs/StressTest.cs +++ b/src/Games/NexusMods.Games.TestHarness/Verbs/StressTest.cs @@ -13,7 +13,7 @@ using NexusMods.ProxyConsole.Abstractions; using NexusMods.ProxyConsole.Abstractions.VerbDefinitions; using NexusMods.StandardGameLocators; -using ModId = NexusMods.Abstractions.NexusWebApi.Types.ModId; +using ModId = NexusMods.Abstractions.NexusWebApi.Types.V2.ModId; namespace NexusMods.Games.TestHarness.Verbs; @@ -38,7 +38,7 @@ internal static async Task RunStressTest( AdvancedManualInstallerUI.Headless = true; var mods = await nexusApiClient.ModUpdatesAsync(game.Domain.Value, PastTime.Day, token); - var results = new List<(string FileName, ModId ModId, Abstractions.NexusWebApi.Types.FileId FileId, Hash Hash, bool Passed, Exception? exception)>(); + var results = new List<(string FileName, ModId ModId, Abstractions.NexusWebApi.Types.V2.FileId FileId, Hash Hash, bool Passed, Exception? exception)>(); await using var gameFolder = temporaryFileManager.CreateFolder(); var (manualId, install) = await manualLocator.Add(game, new Version(1, 0), gameFolder); diff --git a/src/Networking/NexusMods.Networking.ModUpdates/MultiFeedCacheUpdater.cs b/src/Networking/NexusMods.Networking.ModUpdates/MultiFeedCacheUpdater.cs index 29c1bedf4e..166d21efd3 100644 --- a/src/Networking/NexusMods.Networking.ModUpdates/MultiFeedCacheUpdater.cs +++ b/src/Networking/NexusMods.Networking.ModUpdates/MultiFeedCacheUpdater.cs @@ -1,4 +1,4 @@ -using NexusMods.Networking.ModUpdates.Structures; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.Networking.ModUpdates.Traits; namespace NexusMods.Networking.ModUpdates; @@ -12,7 +12,7 @@ namespace NexusMods.Networking.ModUpdates; /// you to use mods and API responses which are sourced from multiple feeds (games), /// as opposed to a single feed. /// -public class MultiFeedCacheUpdater where TUpdateableItem : ICanGetLastUpdatedTimestamp, ICanGetUid +public class MultiFeedCacheUpdater where TUpdateableItem : ICanGetLastUpdatedTimestamp, ICanGetUidForMod { private readonly Dictionary> _updaters; @@ -74,9 +74,9 @@ public MultiFeedCacheUpdater(TUpdateableItem[] items, TimeSpan expiry) /// is automatically detected. /// /// Wrap elements in a struct that implements - /// and if necessary. + /// and if necessary. /// - public void Update(IEnumerable items) where T : ICanGetLastUpdatedTimestamp, ICanGetUid + public void Update(IEnumerable items) where T : ICanGetLastUpdatedTimestamp, ICanGetUidForMod { foreach (var item in items) { diff --git a/src/Networking/NexusMods.Networking.ModUpdates/NexusMods.Networking.ModUpdates.csproj b/src/Networking/NexusMods.Networking.ModUpdates/NexusMods.Networking.ModUpdates.csproj index 91abef7d7e..adbddb4057 100644 --- a/src/Networking/NexusMods.Networking.ModUpdates/NexusMods.Networking.ModUpdates.csproj +++ b/src/Networking/NexusMods.Networking.ModUpdates/NexusMods.Networking.ModUpdates.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdater.cs b/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdater.cs index 3189f1ee7c..f60a075621 100644 --- a/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdater.cs +++ b/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdater.cs @@ -1,6 +1,6 @@ using System.Diagnostics; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.Networking.ModUpdates.Private; -using NexusMods.Networking.ModUpdates.Structures; using NexusMods.Networking.ModUpdates.Traits; namespace NexusMods.Networking.ModUpdates; @@ -12,7 +12,7 @@ namespace NexusMods.Networking.ModUpdates; /// This API consists of the following: /// /// 1. Input [Constructor]: A set of items with a 'last update time' (see ) -/// and a 'unique id' (see ) that are relevant to the current 'feed' (game). +/// and a 'unique id' (see ) that are relevant to the current 'feed' (game). /// /// 2. Update [Method]: Submit results from API endpoint returning 'most recently updated mods for game'. /// This updates the internal state of the . @@ -31,7 +31,7 @@ namespace NexusMods.Networking.ModUpdates; /// The 'Feed' in the context of the Nexus App is the individual game's 'updated.json' endpoint; /// i.e. a 'Game Mod Feed' /// -public class PerFeedCacheUpdater where TUpdateableItem : ICanGetLastUpdatedTimestamp, ICanGetUid +public class PerFeedCacheUpdater where TUpdateableItem : ICanGetLastUpdatedTimestamp, ICanGetUidForMod { private readonly TUpdateableItem[] _items; private readonly Dictionary _itemToIndex; @@ -79,15 +79,15 @@ public PerFeedCacheUpdater(TUpdateableItem[] items, TimeSpan expiry) /// /// The items returned by the 'most recently updated mods for game' endpoint. /// Wrap elements in a struct that implements - /// and if necessary. + /// and if necessary. /// - public void Update(IEnumerable items) where T : ICanGetLastUpdatedTimestamp, ICanGetUid + public void Update(IEnumerable items) where T : ICanGetLastUpdatedTimestamp, ICanGetUidForMod { foreach (var item in items) UpdateSingleItem(item); } - internal void UpdateSingleItem(T item) where T : ICanGetLastUpdatedTimestamp, ICanGetUid + internal void UpdateSingleItem(T item) where T : ICanGetLastUpdatedTimestamp, ICanGetUidForMod { // Try to get index of the item. // Not all the items from the update feed are locally stored, thus we need to diff --git a/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdaterResult.cs b/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdaterResult.cs index 1f14548a1c..78287340c4 100644 --- a/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdaterResult.cs +++ b/src/Networking/NexusMods.Networking.ModUpdates/PerFeedCacheUpdaterResult.cs @@ -6,7 +6,7 @@ namespace NexusMods.Networking.ModUpdates; /// of feeds. /// /// Wrapper for item supported by the cache updater. -public class PerFeedCacheUpdaterResult where TUpdateableItem : ICanGetLastUpdatedTimestamp, ICanGetUid +public class PerFeedCacheUpdaterResult where TUpdateableItem : ICanGetLastUpdatedTimestamp, ICanGetUidForMod { /// /// This is a list of items that is 'out of date'. diff --git a/src/Networking/NexusMods.Networking.ModUpdates/Traits/ICanGetUidForMod.cs b/src/Networking/NexusMods.Networking.ModUpdates/Traits/ICanGetUidForMod.cs new file mode 100644 index 0000000000..06d417eb05 --- /dev/null +++ b/src/Networking/NexusMods.Networking.ModUpdates/Traits/ICanGetUidForMod.cs @@ -0,0 +1,17 @@ +using NexusMods.Abstractions.NexusWebApi.Types.V2.Uid; +namespace NexusMods.Networking.ModUpdates.Traits; + +/// +/// A trait representing an item which has a unique ID. +/// In this case, a 'unique ID' refers to a NexusMods Mod Page ID. +/// +/// This ID must be truly unique and belong to a specific item of its type. +/// +public interface ICanGetUidForMod +{ + /// + /// Returns a unique identifier for the given item, based on the ID format + /// used in the NexusMods V2 API. + /// + public UidForMod GetUniqueId(); +} diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/ClientExtensions.cs b/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/ClientExtensions.cs index f5f056a701..0cc5f71c38 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/ClientExtensions.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/ClientExtensions.cs @@ -1,6 +1,7 @@ using NexusMods.Abstractions.Games.DTO; using NexusMods.Abstractions.NexusWebApi.DTOs; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; namespace NexusMods.Networking.NexusWebApi.Extensions; diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/FragmentExtensions.cs b/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/FragmentExtensions.cs index 4151a111e1..f6fd50dc8d 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/FragmentExtensions.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/Extensions/FragmentExtensions.cs @@ -2,6 +2,7 @@ using NexusMods.Abstractions.NexusModsLibrary; using NexusMods.Abstractions.NexusModsLibrary.Models; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.MnemonicDB.Abstractions; using NexusMods.Paths; @@ -31,7 +32,7 @@ public static async Task Resolve(this IUserFragment userFragment, IDb /// public static EntityId Resolve(this IModFileFragment modFileFragment, IDb db, ITransaction tx, EntityId modEId) { - var nexusFileResolver = GraphQLResolver.Create(db, tx, (NexusModsFileMetadata.FileId, FileId.From((ulong)modFileFragment.FileId)), (NexusModsFileMetadata.ModPageId, modEId)); + var nexusFileResolver = GraphQLResolver.Create(db, tx, (NexusModsFileMetadata.FileId, FileId.From((uint)modFileFragment.FileId)), (NexusModsFileMetadata.ModPageId, modEId)); nexusFileResolver.Add(NexusModsFileMetadata.ModPageId, modEId); nexusFileResolver.Add(NexusModsFileMetadata.Name, modFileFragment.Name); nexusFileResolver.Add(NexusModsFileMetadata.Version, modFileFragment.Version); @@ -45,8 +46,7 @@ public static EntityId Resolve(this IModFileFragment modFileFragment, IDb db, IT /// public static EntityId Resolve(this IModFragment modFragment, IDb db, ITransaction tx) { - var nexusModResolver = GraphQLResolver.Create(db, tx, NexusModsModPageMetadata.ModId, - ModId.From((ulong)modFragment.ModId)); + var nexusModResolver = GraphQLResolver.Create(db, tx, NexusModsModPageMetadata.ModId, ModId.From((uint)modFragment.ModId)); nexusModResolver.Add(NexusModsModPageMetadata.Name, modFragment.Name); nexusModResolver.Add(NexusModsModPageMetadata.GameDomain, GameDomain.From(modFragment.Game.DomainName)); diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiClient.cs b/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiClient.cs index 2910c37369..fd14a94eba 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiClient.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiClient.cs @@ -6,6 +6,7 @@ using NexusMods.Abstractions.NexusWebApi.DTOs.Interfaces; using NexusMods.Abstractions.NexusWebApi.DTOs.OAuth; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; namespace NexusMods.Networking.NexusWebApi; diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiVerbs.cs b/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiVerbs.cs index 31bfc9a778..aed7d9a13e 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiVerbs.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/NexusApiVerbs.cs @@ -2,6 +2,7 @@ using NexusMods.Abstractions.Cli; using NexusMods.Abstractions.NexusWebApi; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.ProxyConsole.Abstractions; using NexusMods.ProxyConsole.Abstractions.VerbDefinitions; diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.cs b/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.cs index 4047b7794c..21076625f8 100644 --- a/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.cs +++ b/src/Networking/NexusMods.Networking.NexusWebApi/NexusModsLibrary.cs @@ -9,6 +9,7 @@ using NexusMods.Abstractions.NexusWebApi; using NexusMods.Abstractions.NexusWebApi.DTOs; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.Extensions.BCL; using NexusMods.MnemonicDB.Abstractions; using NexusMods.Networking.HttpDownloader; diff --git a/tests/Games/NexusMods.Games.TestFramework/Downloader/NexusModMetadata.cs b/tests/Games/NexusMods.Games.TestFramework/Downloader/NexusModMetadata.cs index 648b332451..a7e84b60b6 100644 --- a/tests/Games/NexusMods.Games.TestFramework/Downloader/NexusModMetadata.cs +++ b/tests/Games/NexusMods.Games.TestFramework/Downloader/NexusModMetadata.cs @@ -1,4 +1,5 @@ using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.Hashing.xxHash64; namespace NexusMods.Games.TestFramework.Downloader; diff --git a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/Helpers/TestItem.cs b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/Helpers/TestItem.cs index 1536926193..cee5ced92a 100644 --- a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/Helpers/TestItem.cs +++ b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/Helpers/TestItem.cs @@ -1,22 +1,23 @@ -using NexusMods.Networking.ModUpdates.Structures; +using NexusMods.Abstractions.NexusWebApi.Types.V2; +using NexusMods.Abstractions.NexusWebApi.Types.V2.Uid; using NexusMods.Networking.ModUpdates.Traits; -namespace NexusMods.Networking.ModUpdates.Tests; +namespace NexusMods.Networking.ModUpdates.Tests.Helpers; // Helper class to simulate updateable items -public class TestItem : ICanGetLastUpdatedTimestamp, ICanGetUid +public class TestItem : ICanGetLastUpdatedTimestamp, ICanGetUidForMod { public DateTime LastUpdated { get; set; } - public Uid Uid { get; set; } + public UidForMod Uid { get; set; } public DateTime GetLastUpdatedDate() => LastUpdated; - public Uid GetUniqueId() => Uid; + public UidForMod GetUniqueId() => Uid; // Helper method to create a test item public static TestItem Create(uint gameId, uint modId, DateTime lastUpdated) { return new TestItem { - Uid = new Uid { GameId = GameId.From(gameId), ModId = ModId.From(modId) }, + Uid = new UidForMod { GameId = GameId.From(gameId), ModId = ModId.From(modId) }, LastUpdated = lastUpdated, }; } diff --git a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/MultiFeedCacheUpdaterTests.cs b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/MultiFeedCacheUpdaterTests.cs index 3f578e0d6e..63a8b7749b 100644 --- a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/MultiFeedCacheUpdaterTests.cs +++ b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/MultiFeedCacheUpdaterTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; -using static NexusMods.Networking.ModUpdates.Tests.TestItem; +using NexusMods.Networking.ModUpdates.Tests.Helpers; +using static NexusMods.Networking.ModUpdates.Tests.Helpers.TestItem; namespace NexusMods.Networking.ModUpdates.Tests; diff --git a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/PerFeedCacheUpdaterTests.cs b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/PerFeedCacheUpdaterTests.cs index e6acd44711..7abcd9ad4a 100644 --- a/tests/Networking/NexusMods.Networking.ModUpdates.Tests/PerFeedCacheUpdaterTests.cs +++ b/tests/Networking/NexusMods.Networking.ModUpdates.Tests/PerFeedCacheUpdaterTests.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using FluentAssertions; -using static NexusMods.Networking.ModUpdates.Tests.TestItem; +using NexusMods.Networking.ModUpdates.Tests.Helpers; +using static NexusMods.Networking.ModUpdates.Tests.Helpers.TestItem; namespace NexusMods.Networking.ModUpdates.Tests; diff --git a/tests/Networking/NexusMods.Networking.NexusWebApi.Tests/Types/V2/UidForFileTests.cs b/tests/Networking/NexusMods.Networking.NexusWebApi.Tests/Types/V2/UidForFileTests.cs new file mode 100644 index 0000000000..521101cca1 --- /dev/null +++ b/tests/Networking/NexusMods.Networking.NexusWebApi.Tests/Types/V2/UidForFileTests.cs @@ -0,0 +1,312 @@ +using FluentAssertions; +using NexusMods.Abstractions.NexusWebApi.Types.V2; +using NexusMods.Abstractions.NexusWebApi.Types.V2.Uid; +namespace NexusMods.Networking.NexusWebApi.Tests.Types.V2; + +public class UidForFileTests +{ + [Fact] + public void UidForFile_IsCorrectSize() + { + // Arrange + unsafe + { + // UidForFile does an unsafe cast in FromUlong and AsUlong + // This test ensures nobody tampers with the size of the struct + // or its components; ensuring those unsafe casts are safe. + sizeof(UidForFile).Should().Be(8); + sizeof(FileId).Should().Be(4); + sizeof(GameId).Should().Be(4); + } + } + + [Theory] + [InlineData(1704U, 405U, "7318624272789")] + [InlineData(1704U, 407U, "7318624272791")] + [InlineData(1704U, 406U, "7318624272790")] + [InlineData(1704U, 5564U, "7318624277948")] + [InlineData(1704U, 5565U, "7318624277949")] + [InlineData(1704U, 163337U, "7318624435721")] + [InlineData(1704U, 163338U, "7318624435722")] + [InlineData(1704U, 296267U, "7318624568651")] + [InlineData(1704U, 296268U, "7318624568652")] + [InlineData(3333U, 1U, "14315125997569")] + [InlineData(3333U, 2U, "14315125997570")] + [InlineData(3333U, 41002U, "14315126038570")] + [InlineData(3333U, 4U, "14315125997572")] + public void FromV2Api_ValidInput_ReturnsCorrectUidForFile(uint expectedGameId, uint expectedFileId, string uidString) + { + // Act + var result = UidForFile.FromV2Api(uidString); + + // Assert + result.GameId.Should().Be((GameId)expectedGameId); + result.FileId.Should().Be((FileId)expectedFileId); + } + + [Fact] + public void FromV2Api_InvalidInput_ThrowsFormatException() + { + // Arrange + var invalidUid = "not a number"; + + // Act & Assert + Action act = () => UidForFile.FromV2Api(invalidUid); + act.Should().Throw(); + } + + [Theory] + [InlineData(1704U, 405U, 7318624272789UL)] + [InlineData(1704U, 407U, 7318624272791UL)] + [InlineData(1704U, 406U, 7318624272790UL)] + [InlineData(3333U, 1U, 14315125997569UL)] + [InlineData(3333U, 2U, 14315125997570UL)] + public void AsUlong_ReturnsCorrectValue(uint gameId, uint fileId, ulong expectedUlong) + { + // Arrange + var uidForFile = new UidForFile { GameId = (GameId)gameId, FileId = (FileId)fileId }; + + // Act + var result = uidForFile.AsUlong; + + // Assert + result.Should().Be(expectedUlong); + } + + [Theory] + [InlineData(7318624272789UL, 1704U, 405U)] + [InlineData(7318624272791UL, 1704U, 407U)] + [InlineData(7318624272790UL, 1704U, 406U)] + [InlineData(14315125997569UL, 3333U, 1U)] + [InlineData(14315125997570UL, 3333U, 2U)] + public void FromUlong_ReturnsCorrectUidForFile(ulong input, uint expectedGameId, uint expectedFileId) + { + // Act + var result = UidForFile.FromUlong(input); + + // Assert + result.GameId.Should().Be((GameId)expectedGameId); + result.FileId.Should().Be((FileId)expectedFileId); + } + + [Theory] + [InlineData(1704U, 405U)] + [InlineData(3333U, 1U)] + public void RoundTrip_UlongConversion_PreservesValues(uint gameId, uint fileId) + { + // Arrange + var original = new UidForFile { GameId = (GameId)gameId, FileId = (FileId)fileId }; + + // Act + var asUlong = original.AsUlong; + var roundTripped = UidForFile.FromUlong(asUlong); + + // Assert + roundTripped.Should().Be(original); + } +} + + +/* + Deriveration of test cases. + + Original Request(s): + + ``` + query ModFiles($modId: ID!, $gameId: ID!) { + modFiles(modId: $modId, gameId: $gameId) { + fileId + uid + } + } + ``` + + Input: + ``` + { + "modId": 1, + "gameId": "1704" + } + ``` + + Response: + ```json + { + "data": { + "modFiles": [ + { + "fileId": 405, + "uid": "7318624272789" + } + ] + } + } + ``` + + Input: + ``` + { + "modId": 2, + "gameId": "1704" + } + ``` + + Response: + ```json + { + "data": { + "modFiles": [ + { + "fileId": 407, + "uid": "7318624272791" + } + ] + } + } + ``` + + Input: + ``` + { + "modId": 3, + "gameId": "1704" + } + ``` + + Response: + ```json + { + "data": { + "modFiles": [ + { + "fileId": 406, + "uid": "7318624272790" + } + ] + } + } + ``` + + Input: + ``` + { + "modId": 1000, + "gameId": "1704" + } + ``` + + Response: + ```json + { + "data": { + "modFiles": [ + { + "fileId": 5564, + "uid": "7318624277948" + }, + { + "fileId": 5565, + "uid": "7318624277949" + }, + { + "fileId": 163337, + "uid": "7318624435721" + }, + { + "fileId": 163338, + "uid": "7318624435722" + }, + { + "fileId": 296267, + "uid": "7318624568651" + }, + { + "fileId": 296268, + "uid": "7318624568652" + } + ] + } + } + ``` + + Input: + ``` + { + "modId": 1, + "gameId": "3333" + } + ``` + + Response: + ```json + { + "data": { + "modFiles": [ + { + "fileId": 1, + "uid": "14315125997569" + }, + { + "fileId": 2, + "uid": "14315125997570" + }, + { + "fileId": 41002, + "uid": "14315126038570" + } + ] + } + } + ``` + + Input: + ``` + { + "modId": 1, + "gameId": "3333" + } + ``` + + Response: + ```json + { + "data": { + "modFiles": [ + { + "fileId": 1, + "uid": "14315125997569" + }, + { + "fileId": 2, + "uid": "14315125997570" + }, + { + "fileId": 41002, + "uid": "14315126038570" + } + ] + } + } + ``` + + Input: + ``` + { + "modId": 3, + "gameId": "3333" + } + ``` + + Response: + ```json + { + "data": { + "modFiles": [ + { + "fileId": 4, + "uid": "14315125997572" + } + ] + } + } +*/ diff --git a/tests/Networking/NexusMods.Networking.NexusWebApi.Tests/Types/V2/UidForModTests.cs b/tests/Networking/NexusMods.Networking.NexusWebApi.Tests/Types/V2/UidForModTests.cs new file mode 100644 index 0000000000..f93af91551 --- /dev/null +++ b/tests/Networking/NexusMods.Networking.NexusWebApi.Tests/Types/V2/UidForModTests.cs @@ -0,0 +1,400 @@ +using FluentAssertions; +using NexusMods.Abstractions.NexusWebApi.Types.V2; +using NexusMods.Abstractions.NexusWebApi.Types.V2.Uid; +namespace NexusMods.Networking.NexusWebApi.Tests.Types.V2; + +public class UidForModTests +{ + [Fact] + public void UidForMod_IsCorrectSize() + { + // Arrange + unsafe + { + // UidForMod does an unsafe cast in FromUlong and AsUlong + // This test ensures nobody tampers with the size of the struct + // or its components; ensuring those unsafe casts are safe. + sizeof(UidForMod).Should().Be(8); + sizeof(GameId).Should().Be(4); + sizeof(FileId).Should().Be(4); + } + } + + [Theory] + [InlineData(1704U, 130248U, "7318624402632")] + [InlineData(1704U, 130167U, "7318624402551")] + [InlineData(1704U, 130246U, "7318624402630")] + [InlineData(1704U, 130245U, "7318624402629")] + [InlineData(1704U, 130243U, "7318624402627")] + [InlineData(1704U, 130244U, "7318624402628")] + [InlineData(1704U, 130242U, "7318624402626")] + [InlineData(1704U, 130240U, "7318624402624")] + [InlineData(1704U, 130241U, "7318624402625")] + [InlineData(1704U, 130191U, "7318624402575")] + [InlineData(1704U, 130239U, "7318624402623")] + [InlineData(1704U, 129994U, "7318624402378")] + [InlineData(1704U, 130237U, "7318624402621")] + [InlineData(1704U, 130238U, "7318624402622")] + [InlineData(1704U, 130234U, "7318624402618")] + [InlineData(1704U, 130235U, "7318624402619")] + [InlineData(1704U, 130230U, "7318624402614")] + [InlineData(1704U, 130233U, "7318624402617")] + [InlineData(1704U, 130232U, "7318624402616")] + [InlineData(1704U, 130231U, "7318624402615")] + [InlineData(2500U, 76U, "10737418240076")] + [InlineData(2500U, 75U, "10737418240075")] + [InlineData(2500U, 74U, "10737418240074")] + [InlineData(2500U, 73U, "10737418240073")] + [InlineData(2500U, 72U, "10737418240072")] + [InlineData(2500U, 70U, "10737418240070")] + [InlineData(2500U, 69U, "10737418240069")] + [InlineData(2500U, 68U, "10737418240068")] + [InlineData(2500U, 67U, "10737418240067")] + [InlineData(2500U, 66U, "10737418240066")] + [InlineData(2500U, 65U, "10737418240065")] + [InlineData(2500U, 64U, "10737418240064")] + [InlineData(2500U, 63U, "10737418240063")] + [InlineData(2500U, 62U, "10737418240062")] + [InlineData(2500U, 60U, "10737418240060")] + [InlineData(2500U, 59U, "10737418240059")] + [InlineData(2500U, 58U, "10737418240058")] + [InlineData(2500U, 57U, "10737418240057")] + [InlineData(2500U, 56U, "10737418240056")] + [InlineData(2500U, 55U, "10737418240055")] + public void FromV2Api_ValidInput_ReturnsCorrectUidForMod(uint expectedGameId, uint expectedModId, string uidString) + { + // Act + var result = UidForMod.FromV2Api(uidString); + + // Assert + result.GameId.Should().Be((GameId)expectedGameId); + result.ModId.Should().Be((ModId)expectedModId); + } + + [Fact] + public void FromV2Api_InvalidInput_ThrowsFormatException() + { + // Arrange + var invalidUid = "not a number"; + + // Act & Assert + Action act = () => UidForMod.FromV2Api(invalidUid); + act.Should().Throw(); + } + + [Theory] + [InlineData(1704U, 130248U, 7318624402632UL)] + [InlineData(1704U, 130167U, 7318624402551UL)] + [InlineData(1704U, 130246U, 7318624402630UL)] + [InlineData(1704U, 130245U, 7318624402629UL)] + [InlineData(1704U, 130243U, 7318624402627UL)] + [InlineData(2500U, 76U, 10737418240076UL)] + [InlineData(2500U, 75U, 10737418240075UL)] + [InlineData(2500U, 74U, 10737418240074UL)] + [InlineData(2500U, 73U, 10737418240073UL)] + [InlineData(2500U, 72U, 10737418240072UL)] + public void AsUlong_ReturnsCorrectValue(uint gameId, uint modId, ulong expectedUlong) + { + // Arrange + var uidForMod = new UidForMod { GameId = (GameId)gameId, ModId = (ModId)modId }; + + // Act + var result = uidForMod.AsUlong; + + // Assert + result.Should().Be(expectedUlong); + } + + [Theory] + [InlineData(7318624402632UL, 1704U, 130248U)] + [InlineData(7318624402551UL, 1704U, 130167U)] + [InlineData(7318624402630UL, 1704U, 130246U)] + [InlineData(7318624402629UL, 1704U, 130245U)] + [InlineData(7318624402627UL, 1704U, 130243U)] + [InlineData(10737418240076UL, 2500U, 76U)] + [InlineData(10737418240075UL, 2500U, 75U)] + [InlineData(10737418240074UL, 2500U, 74U)] + [InlineData(10737418240073UL, 2500U, 73U)] + [InlineData(10737418240072UL, 2500U, 72U)] + public void FromUlong_ReturnsCorrectUidForMod(ulong input, uint expectedGameId, uint expectedModId) + { + // Act + var result = UidForMod.FromUlong(input); + + // Assert + result.GameId.Should().Be((GameId)expectedGameId); + result.ModId.Should().Be((ModId)expectedModId); + } + + [Theory] + [InlineData(1704U, 130248U)] + [InlineData(2500U, 76U)] + public void RoundTrip_UlongConversion_PreservesValues(uint gameId, uint modId) + { + // Arrange + var original = new UidForMod { GameId = (GameId)gameId, ModId = (ModId)modId }; + + // Act + var asUlong = original.AsUlong; + var roundTripped = UidForMod.FromUlong(asUlong); + + // Assert + roundTripped.Should().Be(original); + } +} + + +/* + Deriveration of test cases. + + Original Request(s): + + ``` + query Mods { + mods(filter: { gameId: { value: "1704", op: EQUALS } }) { + nodes { + gameId + modId + uid + } + } + } + ``` + + ``` + query Mods { + mods(filter: { gameId: { value: "2500", op: EQUALS } }) { + nodes { + gameId + modId + uid + } + } + } + ``` + + Original Response(s): + + ```json + { + "data": { + "mods": { + "nodes": [ + { + "gameId": 1704, + "modId": 130248, + "uid": "7318624402632" + }, + { + "gameId": 1704, + "modId": 130167, + "uid": "7318624402551" + }, + { + "gameId": 1704, + "modId": 130246, + "uid": "7318624402630" + }, + { + "gameId": 1704, + "modId": 130245, + "uid": "7318624402629" + }, + { + "gameId": 1704, + "modId": 130243, + "uid": "7318624402627" + }, + { + "gameId": 1704, + "modId": 130244, + "uid": "7318624402628" + }, + { + "gameId": 1704, + "modId": 130242, + "uid": "7318624402626" + }, + { + "gameId": 1704, + "modId": 130240, + "uid": "7318624402624" + }, + { + "gameId": 1704, + "modId": 130241, + "uid": "7318624402625" + }, + { + "gameId": 1704, + "modId": 130191, + "uid": "7318624402575" + }, + { + "gameId": 1704, + "modId": 130239, + "uid": "7318624402623" + }, + { + "gameId": 1704, + "modId": 129994, + "uid": "7318624402378" + }, + { + "gameId": 1704, + "modId": 130237, + "uid": "7318624402621" + }, + { + "gameId": 1704, + "modId": 130238, + "uid": "7318624402622" + }, + { + "gameId": 1704, + "modId": 130234, + "uid": "7318624402618" + }, + { + "gameId": 1704, + "modId": 130235, + "uid": "7318624402619" + }, + { + "gameId": 1704, + "modId": 130230, + "uid": "7318624402614" + }, + { + "gameId": 1704, + "modId": 130233, + "uid": "7318624402617" + }, + { + "gameId": 1704, + "modId": 130232, + "uid": "7318624402616" + }, + { + "gameId": 1704, + "modId": 130231, + "uid": "7318624402615" + } + ] + } + } + } + ``` + + ```json + { + "data": { + "mods": { + "nodes": [ + { + "gameId": 2500, + "modId": 76, + "uid": "10737418240076" + }, + { + "gameId": 2500, + "modId": 75, + "uid": "10737418240075" + }, + { + "gameId": 2500, + "modId": 74, + "uid": "10737418240074" + }, + { + "gameId": 2500, + "modId": 73, + "uid": "10737418240073" + }, + { + "gameId": 2500, + "modId": 72, + "uid": "10737418240072" + }, + { + "gameId": 2500, + "modId": 70, + "uid": "10737418240070" + }, + { + "gameId": 2500, + "modId": 69, + "uid": "10737418240069" + }, + { + "gameId": 2500, + "modId": 68, + "uid": "10737418240068" + }, + { + "gameId": 2500, + "modId": 67, + "uid": "10737418240067" + }, + { + "gameId": 2500, + "modId": 66, + "uid": "10737418240066" + }, + { + "gameId": 2500, + "modId": 65, + "uid": "10737418240065" + }, + { + "gameId": 2500, + "modId": 64, + "uid": "10737418240064" + }, + { + "gameId": 2500, + "modId": 63, + "uid": "10737418240063" + }, + { + "gameId": 2500, + "modId": 62, + "uid": "10737418240062" + }, + { + "gameId": 2500, + "modId": 60, + "uid": "10737418240060" + }, + { + "gameId": 2500, + "modId": 59, + "uid": "10737418240059" + }, + { + "gameId": 2500, + "modId": 58, + "uid": "10737418240058" + }, + { + "gameId": 2500, + "modId": 57, + "uid": "10737418240057" + }, + { + "gameId": 2500, + "modId": 56, + "uid": "10737418240056" + }, + { + "gameId": 2500, + "modId": 55, + "uid": "10737418240055" + } + ] + } + } + } + ``` + +*/ diff --git a/tests/NexusMods.UI.Tests/ImageLoaderTests.cs b/tests/NexusMods.UI.Tests/ImageLoaderTests.cs index 8dc43df9f4..03732e8e5f 100644 --- a/tests/NexusMods.UI.Tests/ImageLoaderTests.cs +++ b/tests/NexusMods.UI.Tests/ImageLoaderTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using NexusMods.Abstractions.NexusModsLibrary; using NexusMods.Abstractions.NexusWebApi.Types; +using NexusMods.Abstractions.NexusWebApi.Types.V2; using NexusMods.Abstractions.Resources; using NexusMods.Abstractions.Resources.Caching; using NexusMods.Abstractions.Resources.DB;