diff --git a/Kontent.Ai.Management.Tests/ManagementClientTests/ConstructorTests.cs b/Kontent.Ai.Management.Tests/ManagementClientTests/ConstructorTests.cs new file mode 100644 index 00000000..103c858d --- /dev/null +++ b/Kontent.Ai.Management.Tests/ManagementClientTests/ConstructorTests.cs @@ -0,0 +1,62 @@ +using FluentAssertions; +using Kontent.Ai.Management.Configuration; +using System; +using System.Net.Http; +using Xunit; + +namespace Kontent.Ai.Management.Tests.ManagementClientTests; + +public class ConstructorTests +{ + private static ManagementClient CreateClient(ManagementOptions options, bool useHttpClient) + => useHttpClient + ? new ManagementClient(options, new HttpClient()) + : new ManagementClient(options); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Ctor_ValidOptions_DoesNotThrow(bool useHttpClient) + { + var options = new ManagementOptions + { + EnvironmentId = Guid.NewGuid().ToString(), + ApiKey = "valid-key" + }; + + Action act = () => CreateClient(options, useHttpClient); + act.Should().NotThrow(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Ctor_NullOptions_ThrowsArgumentNull(bool useHttpClient) + { + Action act = () => CreateClient(null!, useHttpClient); + + act.Should().Throw(); + } + + [Theory] + [InlineData("", "key", "environment identifier is not specified")] + [InlineData("no-guid", "key", "not a valid environment identifier")] + [InlineData("4ee3d5cc-2e5b-4c81-9f4c-6a8f7b5d3c1e", "", "API key is not specified")] + public void Ctor_InvalidOptions_ThrowsArgument( + string envId, + string apiKey, + string messagePart) + { + var options = new ManagementOptions + { + EnvironmentId = envId, + ApiKey = apiKey + }; + + Action act = () => CreateClient(options, false); + + act.Should() + .Throw() + .WithMessage($"*{messagePart}*"); + } +} diff --git a/Kontent.Ai.Management.Tests/Modules/ModelBuilders/BaseElementRoundTripTests.cs b/Kontent.Ai.Management.Tests/Modules/ModelBuilders/BaseElementRoundTripTests.cs new file mode 100644 index 00000000..5c59253a --- /dev/null +++ b/Kontent.Ai.Management.Tests/Modules/ModelBuilders/BaseElementRoundTripTests.cs @@ -0,0 +1,149 @@ +using FluentAssertions; +using Kontent.Ai.Management.Models.LanguageVariants.Elements; +using Kontent.Ai.Management.Models.Shared; +using Kontent.Ai.Management.Models.Types.Elements; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Kontent.Ai.Management.Tests.Modules.ModelBuilders; + +public class BaseElementRoundTripTests +{ + public static IEnumerable ReferenceElementTestCases() + { + yield return new object[] { + new TaxonomyElement { + Element = Reference.ByCodename("tax"), + Value = [Reference.ByCodename("t1"), Reference.ByCodename("t2")] + }, + ElementMetadataType.Taxonomy, + new Action(converted => { + var e = (TaxonomyElement)converted; + e.Element.Codename.Should().Be("tax"); + e.Value.Should().HaveCount(2); + e.Value.First().Codename.Should().Be("t1"); + e.Value.Last().Codename.Should().Be("t2"); + }) + }; + + yield return new object[] { + new LinkedItemsElement { + Element = Reference.ByCodename("link"), + Value = [Reference.ByCodename("i1"), Reference.ByExternalId("ext2")] + }, + ElementMetadataType.LinkedItems, + new Action(converted => { + var e = (LinkedItemsElement)converted; + e.Element.Codename.Should().Be("link"); + e.Value.Should().HaveCount(2); + e.Value.First().Codename.Should().Be("i1"); + e.Value.Last().ExternalId.Should().Be("ext2"); + }) + }; + + yield return new object[] { + new MultipleChoiceElement { + Element = Reference.ByCodename("mc"), + Value = [Reference.ByCodename("o1"), Reference.ByCodename("o2")] + }, + ElementMetadataType.MultipleChoice, + new Action(converted => { + var e = (MultipleChoiceElement)converted; + e.Element.Codename.Should().Be("mc"); + e.Value.Should().HaveCount(2); + e.Value.First().Codename.Should().Be("o1"); + e.Value.Last().Codename.Should().Be("o2"); + }) + }; + + yield return new object[] { + new SubpagesElement { + Element = Reference.ByCodename("sp"), + Value = [Reference.ByCodename("p1"), Reference.ByCodename("p2")] + }, + ElementMetadataType.Subpages, + new Action(converted => { + var e = (SubpagesElement)converted; + e.Element.Codename.Should().Be("sp"); + e.Value.Should().HaveCount(2); + e.Value.First().Codename.Should().Be("p1"); + e.Value.Last().Codename.Should().Be("p2"); + }) + }; + } + + [Theory] + [MemberData(nameof(ReferenceElementTestCases))] + public void ReferenceElements_RoundTrip_PreservesValues(BaseElement original, ElementMetadataType type, Action assertConverted) + { + // Act + var dynamic = original.ToDynamic(); + var converted = BaseElement.FromDynamic(dynamic, type); + + // Assert + assertConverted(converted); + } + + public static IEnumerable AssetElementTestCases() + { + yield return new object[] { + new AssetElement { + Element = Reference.ByCodename("asset"), + Value = [ + new AssetWithRenditionsReference(Reference.ByCodename("a1"), [Reference.ByCodename("r1")]), + new AssetWithRenditionsReference(Reference.ByCodename("a2")) + ] + }, + new Action(converted => { + var assets = converted.Value.ToArray(); + converted.Element.Codename.Should().Be("asset"); + assets[0].Codename.Should().Be("a1"); + assets[0].Renditions.Should().HaveCount(1); + assets[0].Renditions.First().Codename.Should().Be("r1"); + assets[1].Codename.Should().Be("a2"); + assets[1].Renditions.Should().BeEmpty(); + }) + }; + + yield return new object[] { + new AssetElement { + Element = Reference.ByCodename("asset"), + Value = [ + new AssetWithRenditionsReference(Reference.ById(Guid.NewGuid()), [Reference.ByCodename("rn")]), + new AssetWithRenditionsReference(Reference.ByExternalId("ext_a"), [Reference.ByExternalId("ext_r")]) + ] + }, + new Action(converted => { + var assets = converted.Value.ToArray(); + assets[0].Id.Should().NotBeNull(); + assets[0].Renditions.First().Codename.Should().Be("rn"); + assets[1].ExternalId.Should().Be("ext_a"); + assets[1].Renditions.First().ExternalId.Should().Be("ext_r"); + }) + }; + + yield return new object[] { + new AssetElement { + Element = Reference.ByCodename("asset"), + Value = null + }, + new Action(converted => { + converted.Value.Should().BeNull(); + }) + }; + } + + [Theory] + [MemberData(nameof(AssetElementTestCases))] + public void AssetElement_RoundTrip_PreservesValues(AssetElement original, Action assertConverted) + { + // Act + var dynamic = original.ToDynamic(); + var converted = (AssetElement)BaseElement.FromDynamic(dynamic, ElementMetadataType.Asset); + + // Assert + assertConverted(converted); + } +} diff --git a/Kontent.Ai.Management.Tests/Modules/ModelBuilders/ElementModelProviderTests.cs b/Kontent.Ai.Management.Tests/Modules/ModelBuilders/ElementModelProviderTests.cs index bf4e6a01..c67f4373 100644 --- a/Kontent.Ai.Management.Tests/Modules/ModelBuilders/ElementModelProviderTests.cs +++ b/Kontent.Ai.Management.Tests/Modules/ModelBuilders/ElementModelProviderTests.cs @@ -85,19 +85,19 @@ public void GetLanguageVariantUpsertModel_ReturnsExpected() var relatedArticlesValue = dynamicElements.SingleOrDefault(elementObject => elementObject.element.id == type.GetProperty(nameof(model.RelatedArticles))?.GetKontentElementId() - ).value as IEnumerable; + ).value as IEnumerable; var teaserImageValue = dynamicElements.SingleOrDefault(elementObject => elementObject.element.id == type.GetProperty(nameof(model.TeaserImage))?.GetKontentElementId() - ).value as IEnumerable; + ).value as IEnumerable; var personaValue = dynamicElements.SingleOrDefault(elementObject => elementObject.element.id == type.GetProperty(nameof(model.Personas))?.GetKontentElementId() - ).value as IEnumerable; + ).value as IEnumerable; var optionsValue = dynamicElements.SingleOrDefault(elementObject => elementObject.element.id == type.GetProperty(nameof(model.Options))?.GetKontentElementId() - ).value as IEnumerable; + ).value as IEnumerable; Assert.Equal(model.Title.Value, titleValue); Assert.Equal(model.Rating.Value, ratingValue); @@ -110,10 +110,10 @@ public void GetLanguageVariantUpsertModel_ReturnsExpected() Assert.Equal(model.BodyCopy.Value, bodyCopyElement.value); Assert.Single(bodyCopyElement.components as IEnumerable); AssertIdentifiers(model.BodyCopy.Components.Select(x => x.Id), (bodyCopyElement.components as IEnumerable)?.Select(x => x.Id)); - AssertIdentifiers(model.RelatedArticles.Value.Select(x => x.Id.Value), relatedArticlesValue.Select(x => x.Id.Value)); - AssertIdentifiers(model.TeaserImage.Value.Select(x => x.Id.Value), teaserImageValue.Select(x => x.Id.Value)); - AssertIdentifiers(model.Personas.Value.Select(x => x.Id.Value), personaValue.Select(x => x.Id.Value)); - AssertIdentifiers(model.Options.Value.Select(x => x.Id.Value), optionsValue.Select(x => x.Id.Value)); + AssertIdentifiers(model.RelatedArticles.Value.Select(x => x.Id.Value), relatedArticlesValue?.Select(r => Reference.FromDynamic(r).Id ?? Guid.Empty).Cast() ?? Enumerable.Empty()); + AssertIdentifiers(model.TeaserImage.Value.Select(x => x.Id.Value), teaserImageValue?.Select(a => AssetWithRenditionsReference.FromDynamic(a).Id ?? Guid.Empty).Cast() ?? Enumerable.Empty()); + AssertIdentifiers(model.Personas.Value.Select(x => x.Id.Value), personaValue?.Select(p => Reference.FromDynamic(p).Id ?? Guid.Empty).Cast() ?? Enumerable.Empty()); + AssertIdentifiers(model.Options.Value.Select(x => x.Id.Value), optionsValue?.Select(o => Reference.FromDynamic(o).Id ?? Guid.Empty).Cast() ?? Enumerable.Empty()); } private static ComplexTestModel GetTestModel() diff --git a/Kontent.Ai.Management.Tests/Modules/ModelBuilders/ElementReferenceTests.cs b/Kontent.Ai.Management.Tests/Modules/ModelBuilders/ElementReferenceTests.cs index e9dc410f..7caa5814 100644 --- a/Kontent.Ai.Management.Tests/Modules/ModelBuilders/ElementReferenceTests.cs +++ b/Kontent.Ai.Management.Tests/Modules/ModelBuilders/ElementReferenceTests.cs @@ -116,5 +116,23 @@ public void ToDynamic_NoIdentifier_ThrowException() .WithMessage("Element reference does not contain any identifier."); } + [Fact] + public void AssetWithRenditionsReference_FromDynamic_WithInvalidData_ThrowsDataMisalignedException() + { + Action action = () => AssetWithRenditionsReference.FromDynamic(new { invalidProperty = "invalid" }); + + action.Should() + .Throw() + .WithMessage("Object could not be converted to the strongly-typed AssetWithRenditionsReference. Please check if it has expected properties with expected type"); + } + [Fact] + public void AssetWithRenditionsReference_FromDynamic_WithNullData_ThrowsDataMisalignedException() + { + Action action = () => AssetWithRenditionsReference.FromDynamic(null); + + action.Should() + .Throw() + .WithMessage("Object could not be converted to the strongly-typed AssetWithRenditionsReference. Please check if it has expected properties with expected type"); + } } diff --git a/Kontent.Ai.Management/Models/LanguageVariants/Elements/AssetElement.cs b/Kontent.Ai.Management/Models/LanguageVariants/Elements/AssetElement.cs index 95759f1e..8520ae2d 100644 --- a/Kontent.Ai.Management/Models/LanguageVariants/Elements/AssetElement.cs +++ b/Kontent.Ai.Management/Models/LanguageVariants/Elements/AssetElement.cs @@ -1,6 +1,7 @@ using Kontent.Ai.Management.Models.Shared; using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace Kontent.Ai.Management.Models.LanguageVariants.Elements; @@ -20,6 +21,6 @@ public class AssetElement : BaseElement /// public override dynamic ToDynamic() => new { element = Element.ToDynamic(), - value = Value, + value = Value?.Select(v => v.ToDynamic()), }; } diff --git a/Kontent.Ai.Management/Models/LanguageVariants/Elements/BaseElement.cs b/Kontent.Ai.Management/Models/LanguageVariants/Elements/BaseElement.cs index 3257f0d5..7ca38dec 100644 --- a/Kontent.Ai.Management/Models/LanguageVariants/Elements/BaseElement.cs +++ b/Kontent.Ai.Management/Models/LanguageVariants/Elements/BaseElement.cs @@ -87,11 +87,7 @@ public static BaseElement FromDynamic(dynamic source, Type type) return new AssetElement { Element = Reference.FromDynamic(source.element), - Value = (source.value as IEnumerable)? - .Select(assetWithRenditionsReferences => - new AssetWithRenditionsReference( - Reference.ById(Guid.Parse(assetWithRenditionsReferences.id)), - (assetWithRenditionsReferences.renditions as IEnumerable)?.Select(renditionIdentifier => Reference.ById(Guid.Parse(renditionIdentifier.id))))), + Value = (source.value as IEnumerable)?.Select(AssetWithRenditionsReference.FromDynamic) }; } else if (type == typeof(DateTimeElement)) @@ -108,7 +104,7 @@ public static BaseElement FromDynamic(dynamic source, Type type) return new LinkedItemsElement { Element = Reference.FromDynamic(source.element), - Value = (source.value as IEnumerable)?.Select(identifier => Reference.ById(Guid.Parse(identifier.id))) + Value = (source.value as IEnumerable)?.Select(Reference.FromDynamic) }; } else if (type == typeof(MultipleChoiceElement)) @@ -116,7 +112,7 @@ public static BaseElement FromDynamic(dynamic source, Type type) return new MultipleChoiceElement { Element = Reference.FromDynamic(source.element), - Value = (source.value as IEnumerable)?.Select(identifier => Reference.ById(Guid.Parse(identifier.id))) + Value = (source.value as IEnumerable)?.Select(Reference.FromDynamic) }; } else if (type == typeof(TaxonomyElement)) @@ -124,7 +120,7 @@ public static BaseElement FromDynamic(dynamic source, Type type) return new TaxonomyElement { Element = Reference.FromDynamic(source.element), - Value = (source.value as IEnumerable)?.Select(identifier => Reference.ById(Guid.Parse(identifier.id))) + Value = (source.value as IEnumerable)?.Select(Reference.FromDynamic) }; } else if (type == typeof(UrlSlugElement)) @@ -150,7 +146,7 @@ public static BaseElement FromDynamic(dynamic source, Type type) return new SubpagesElement { Element = Reference.FromDynamic(source.element), - Value = (source.value as IEnumerable)?.Select(identifier => Reference.ById(Guid.Parse(identifier.id))) + Value = (source.value as IEnumerable)?.Select(Reference.FromDynamic) }; } } diff --git a/Kontent.Ai.Management/Models/LanguageVariants/Elements/LinkedItemsElement.cs b/Kontent.Ai.Management/Models/LanguageVariants/Elements/LinkedItemsElement.cs index ba162227..8e070db7 100644 --- a/Kontent.Ai.Management/Models/LanguageVariants/Elements/LinkedItemsElement.cs +++ b/Kontent.Ai.Management/Models/LanguageVariants/Elements/LinkedItemsElement.cs @@ -1,6 +1,7 @@ using Kontent.Ai.Management.Models.Shared; using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace Kontent.Ai.Management.Models.LanguageVariants.Elements; @@ -20,6 +21,6 @@ public class LinkedItemsElement : BaseElement /// public override dynamic ToDynamic() => new { element = Element.ToDynamic(), - value = Value, + value = Value?.Select(v => v.ToDynamic()), }; } diff --git a/Kontent.Ai.Management/Models/LanguageVariants/Elements/MultipleChoiceElement.cs b/Kontent.Ai.Management/Models/LanguageVariants/Elements/MultipleChoiceElement.cs index cf03f9c5..f291667c 100644 --- a/Kontent.Ai.Management/Models/LanguageVariants/Elements/MultipleChoiceElement.cs +++ b/Kontent.Ai.Management/Models/LanguageVariants/Elements/MultipleChoiceElement.cs @@ -1,6 +1,7 @@ using Kontent.Ai.Management.Models.Shared; using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace Kontent.Ai.Management.Models.LanguageVariants.Elements; @@ -20,6 +21,6 @@ public class MultipleChoiceElement : BaseElement /// public override dynamic ToDynamic() => new { element = Element.ToDynamic(), - value = Value, + value = Value?.Select(v => v.ToDynamic()), }; } diff --git a/Kontent.Ai.Management/Models/LanguageVariants/Elements/SubpagesElement.cs b/Kontent.Ai.Management/Models/LanguageVariants/Elements/SubpagesElement.cs index 888fcff0..817b5edb 100644 --- a/Kontent.Ai.Management/Models/LanguageVariants/Elements/SubpagesElement.cs +++ b/Kontent.Ai.Management/Models/LanguageVariants/Elements/SubpagesElement.cs @@ -1,6 +1,7 @@ using Kontent.Ai.Management.Models.Shared; using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace Kontent.Ai.Management.Models.LanguageVariants.Elements; @@ -20,6 +21,6 @@ public class SubpagesElement : BaseElement /// public override dynamic ToDynamic() => new { element = Element.ToDynamic(), - value = Value, + value = Value?.Select(v => v.ToDynamic()), }; } diff --git a/Kontent.Ai.Management/Models/LanguageVariants/Elements/TaxonomyElement.cs b/Kontent.Ai.Management/Models/LanguageVariants/Elements/TaxonomyElement.cs index aec4838f..f8271ea2 100644 --- a/Kontent.Ai.Management/Models/LanguageVariants/Elements/TaxonomyElement.cs +++ b/Kontent.Ai.Management/Models/LanguageVariants/Elements/TaxonomyElement.cs @@ -1,6 +1,7 @@ using Kontent.Ai.Management.Models.Shared; using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace Kontent.Ai.Management.Models.LanguageVariants.Elements; @@ -20,6 +21,6 @@ public class TaxonomyElement : BaseElement /// public override dynamic ToDynamic() => new { element = Element.ToDynamic(), - value = Value, + value = Value?.Select(v => v.ToDynamic()), }; } diff --git a/Kontent.Ai.Management/Models/Shared/AssetWithRenditionsReference.cs b/Kontent.Ai.Management/Models/Shared/AssetWithRenditionsReference.cs index b96702f1..51b0b738 100644 --- a/Kontent.Ai.Management/Models/Shared/AssetWithRenditionsReference.cs +++ b/Kontent.Ai.Management/Models/Shared/AssetWithRenditionsReference.cs @@ -1,3 +1,4 @@ +using Kontent.Ai.Management.Extensions; using Kontent.Ai.Management.Modules.ActionInvoker; using Newtonsoft.Json; using System; @@ -46,7 +47,7 @@ public sealed class AssetWithRenditionsReference /// Asset reference. /// Rendition reference. public AssetWithRenditionsReference(Reference assetReference, Reference renditionReference) - : this(assetReference, new[] { renditionReference }) + : this(assetReference, [renditionReference]) { } @@ -58,6 +59,66 @@ public AssetWithRenditionsReference(Reference assetReference, Reference renditio public AssetWithRenditionsReference(Reference assetReference, IEnumerable renditionReferences = null) { _assetReference = assetReference; - _renditions = renditionReferences?.ToList() ?? new List(); + _renditions = renditionReferences?.ToList() ?? []; + } + + /// + /// Transforms the dynamic object to the + /// + public static AssetWithRenditionsReference FromDynamic(dynamic source) + { + try + { + var assetReference = Reference.FromDynamic(source); + + IEnumerable renditions = null; + if (DynamicExtensions.HasProperty(source, "renditions") && source.renditions != null) + { + renditions = (source.renditions as IEnumerable)?.Select(Reference.FromDynamic); + } + + return new AssetWithRenditionsReference(assetReference, renditions); + } + catch (Exception exception) + { + throw new DataMisalignedException( + "Object could not be converted to the strongly-typed AssetWithRenditionsReference. Please check if it has expected properties with expected type", + exception); + } + } + + /// + /// Transforms the to the dynamic object. + /// + public dynamic ToDynamic() + { + if (Id != null) + { + return new + { + id = Id, + renditions = (_renditions ?? []).Select(r => r.ToDynamic()) + }; + } + + if (Codename != null) + { + return new + { + codename = Codename, + renditions = (_renditions ?? []).Select(r => r.ToDynamic()) + }; + } + + if (ExternalId != null) + { + return new + { + external_id = ExternalId, + renditions = (_renditions ?? []).Select(r => r.ToDynamic()) + }; + } + + return _assetReference.ToDynamic(); } } \ No newline at end of file