diff --git a/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs b/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs index 4df0d422..b33bec10 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs @@ -15,6 +15,11 @@ internal static class Constants /// public static string ApplicationJsonMediaType = "application/json"; + /// + /// application/octet-stream + /// + public static string ApplicationOctetStreamMediaType = "application/octet-stream"; + /// /// Status code: 200 /// diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs index bd5abab9..a052bfe9 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs @@ -258,7 +258,11 @@ public int CompareTo(ODataPath other) private ODataPathKind CalcPathType() { - if (Segments.Any(c => c.Kind == ODataSegmentKind.Ref)) + if (Segments.Any(c => c.Kind == ODataSegmentKind.StreamProperty || c.Kind == ODataSegmentKind.StreamContent)) + { + return ODataPathKind.MediaEntity; + } + else if (Segments.Any(c => c.Kind == ODataSegmentKind.Ref)) { return ODataPathKind.Ref; } diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs index 2a412150..14498a00 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs @@ -36,15 +36,20 @@ public enum ODataPathKind OperationImport, /// - /// Represents an navigation propert path, for example: ~/users/{id}/onedrive + /// Represents an navigation property path, for example: ~/users/{id}/onedrive /// NavigationProperty, /// - /// Represents an navigation propert $ref path, for example: ~/users/{id}/onedrive/$ref + /// Represents an navigation property $ref path, for example: ~/users/{id}/onedrive/$ref /// Ref, + /// + /// Represents a media entity path, for example: ~/me/photo/$value or ~/reports/deviceConfigurationUserActivity/Content + /// + MediaEntity, + /// /// Represents an un-supported/unknown path. /// diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs index 4273bb73..a6b877b9 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs @@ -126,6 +126,7 @@ private void AppendPath(ODataPath path) case ODataPathKind.Entity: case ODataPathKind.EntitySet: case ODataPathKind.Singleton: + case ODataPathKind.MediaEntity: ODataNavigationSourceSegment navigationSourceSegment = (ODataNavigationSourceSegment)path.FirstSegment; if (!_allNavigationSourcePaths.TryGetValue(navigationSourceSegment.EntityType, out IList nsList)) { @@ -182,6 +183,33 @@ private void RetrieveNavigationSourcePaths(IEdmNavigationSource navigationSource AppendPath(path.Clone()); } + // media entity + bool createValuePath = true; + foreach (IEdmStructuralProperty sp in entityType.DeclaredStructuralProperties()) + { + if (sp.Type.AsPrimitive().IsStream()) + { + path.Push(new ODataStreamPropertySegment(sp.Name)); + AppendPath(path.Clone()); + path.Pop(); + } + + if (sp.Name.Equals("content", System.StringComparison.OrdinalIgnoreCase)) + { + createValuePath = false; + } + } + + /* Create a /$value path only if entity has stream and + * does not contain a structural property named Content + */ + if (createValuePath && entityType.HasStream) + { + path.Push(new ODataStreamContentSegment()); + AppendPath(path.Clone()); + path.Pop(); + } + // navigation property foreach (IEdmNavigationProperty np in entityType.DeclaredNavigationProperties()) { @@ -369,7 +397,8 @@ private bool AppendBoundOperationOnNavigationSourcePath(IEdmOperation edmOperati foreach (var subPath in value) { if ((isCollection && subPath.Kind == ODataPathKind.EntitySet) || - (!isCollection && subPath.Kind != ODataPathKind.EntitySet)) + (!isCollection && subPath.Kind != ODataPathKind.EntitySet && + subPath.Kind != ODataPathKind.MediaEntity)) { ODataPath newPath = subPath.Clone(); newPath.Push(new ODataOperationSegment(edmOperation, isEscapedFunction)); diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs index ad43a9f0..b6fb0e8b 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs @@ -47,7 +47,17 @@ public enum ODataSegmentKind /// /// $ref /// - Ref + Ref, + + /// + /// Stream content -> $value + /// + StreamContent, + + /// + /// Stream property + /// + StreamProperty } /// diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamContentSegment.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamContentSegment.cs new file mode 100644 index 00000000..aba66263 --- /dev/null +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamContentSegment.cs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using System.Collections.Generic; + +namespace Microsoft.OpenApi.OData.Edm +{ + /// + /// Stream segment. + /// + public class ODataStreamContentSegment : ODataSegment + { + /// + public override ODataSegmentKind Kind => ODataSegmentKind.StreamContent; + + /// + public override string Identifier => "$value"; + + /// + public override string GetPathItemName(OpenApiConvertSettings settings, HashSet parameters) => "$value"; + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamPropertySegment.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamPropertySegment.cs new file mode 100644 index 00000000..efca6864 --- /dev/null +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataStreamPropertySegment.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using System.Collections.Generic; +using Microsoft.OpenApi.OData.Common; + +namespace Microsoft.OpenApi.OData.Edm +{ + /// + /// Property Stream segment. + /// + public class ODataStreamPropertySegment : ODataSegment + { + private readonly string _streamPropertyName; + /// + /// Initializes a new instance of class. + /// + /// The name of the stream property. + public ODataStreamPropertySegment(string streamPropertyName) + { + _streamPropertyName = streamPropertyName ?? throw Error.ArgumentNull(nameof(streamPropertyName)); + } + + /// + public override ODataSegmentKind Kind => ODataSegmentKind.StreamProperty; + + /// + public override string Identifier { get => _streamPropertyName; } + + /// + public override string GetPathItemName(OpenApiConvertSettings settings, HashSet parameters) => _streamPropertyName; + } +} diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs new file mode 100644 index 00000000..fa6cf701 --- /dev/null +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs @@ -0,0 +1,109 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using Microsoft.OData.Edm; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.OData.Common; +using Microsoft.OpenApi.OData.Edm; +using Microsoft.OpenApi.OData.Generator; +using Microsoft.OpenApi.OData.Vocabulary.Capabilities; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.OpenApi.OData.Operation +{ + /// + /// Retrieve a media content for an Entity + /// + internal class MediaEntityGetOperationHandler : EntitySetOperationHandler + { + /// + public override OperationType OperationType => OperationType.Get; + + /// + protected override void SetBasicInfo(OpenApiOperation operation) + { + string typeName = EntitySet.EntityType().Name; + + // Summary + operation.Summary = $"Get media content for {typeName} from {EntitySet.Name}"; + + // OperationId + if (Context.Settings.EnableOperationId) + { + string identifier = Path.LastSegment.Kind == ODataSegmentKind.StreamContent ? "Content" : Path.LastSegment.Identifier; + operation.OperationId = EntitySet.Name + "." + typeName + ".Get" + Utils.UpperFirstChar(identifier); + } + + base.SetBasicInfo(operation); + } + + /// + protected override void SetResponses(OpenApiOperation operation) + { + OpenApiSchema schema = null; + + if (Context.Settings.EnableDerivedTypesReferencesForResponses) + { + schema = EdmModelHelper.GetDerivedTypesReferenceSchema(EntitySet.EntityType(), Context.Model); + } + + if (schema == null) + { + schema = new OpenApiSchema + { + Type = "string", + Format = "binary" + }; + } + + operation.Responses = new OpenApiResponses + { + { + Constants.StatusCode200, + new OpenApiResponse + { + Description = "Retrieved media content", + Content = new Dictionary + { + { + Constants.ApplicationOctetStreamMediaType, + new OpenApiMediaType + { + Schema = schema + } + } + } + } + } + }; + operation.Responses.Add(Constants.StatusCodeDefault, Constants.StatusCodeDefault.GetResponse()); + + base.SetResponses(operation); + } + /// + protected override void SetSecurity(OpenApiOperation operation) + { + ReadRestrictionsType read = Context.Model.GetRecord(EntitySet, CapabilitiesConstants.ReadRestrictions); + if (read == null) + { + return; + } + + ReadRestrictionsBase readBase = read; + if (read.ReadByKeyRestrictions != null) + { + readBase = read.ReadByKeyRestrictions; + } + + if (readBase == null && readBase.Permissions == null) + { + return; + } + + operation.Security = Context.CreateSecurityRequirements(readBase.Permissions).ToList(); + } + } +} diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs new file mode 100644 index 00000000..a8ca7cf9 --- /dev/null +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs @@ -0,0 +1,104 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OData.Edm; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.OData.Common; +using Microsoft.OpenApi.OData.Edm; +using Microsoft.OpenApi.OData.Generator; +using Microsoft.OpenApi.OData.Vocabulary.Capabilities; + +namespace Microsoft.OpenApi.OData.Operation +{ + /// + /// Update a media content for an Entity + /// + internal class MediaEntityPutOperationHandler : EntitySetOperationHandler + { + /// + public override OperationType OperationType => OperationType.Put; + + /// + protected override void SetBasicInfo(OpenApiOperation operation) + { + string typeName = EntitySet.EntityType().Name; + + // Summary + operation.Summary = $"Update media content for {typeName} in {EntitySet.Name}"; + + // OperationId + if (Context.Settings.EnableOperationId) + { + string identifier = Path.LastSegment.Kind == ODataSegmentKind.StreamContent ? "Content" : Path.LastSegment.Identifier; + operation.OperationId = EntitySet.Name + "." + typeName + ".Update" + Utils.UpperFirstChar(identifier); + } + + base.SetBasicInfo(operation); + } + + /// + protected override void SetRequestBody(OpenApiOperation operation) + { + OpenApiSchema schema = null; + + if (Context.Settings.EnableDerivedTypesReferencesForRequestBody) + { + schema = EdmModelHelper.GetDerivedTypesReferenceSchema(EntitySet.EntityType(), Context.Model); + } + + if (schema == null) + { + schema = new OpenApiSchema + { + Type = "string", + Format = "binary" + }; + } + + operation.RequestBody = new OpenApiRequestBody + { + Required = true, + Description = "New media content.", + Content = new Dictionary + { + { + Constants.ApplicationOctetStreamMediaType, new OpenApiMediaType + { + Schema = schema + } + } + } + }; + + base.SetRequestBody(operation); + } + + /// + protected override void SetResponses(OpenApiOperation operation) + { + operation.Responses = new OpenApiResponses + { + { Constants.StatusCode204, Constants.StatusCode204.GetResponse() }, + { Constants.StatusCodeDefault, Constants.StatusCodeDefault.GetResponse() } + }; + + base.SetResponses(operation); + } + + /// + protected override void SetSecurity(OpenApiOperation operation) + { + UpdateRestrictionsType update = Context.Model.GetRecord(EntitySet, CapabilitiesConstants.UpdateRestrictions); + if (update == null || update.Permissions == null) + { + return; + } + + operation.Security = Context.CreateSecurityRequirements(update.Permissions).ToList(); + } + } +} diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/OperationHandlerProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/OperationHandlerProvider.cs index 763f84a8..3d3abce2 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/OperationHandlerProvider.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/OperationHandlerProvider.cs @@ -76,6 +76,13 @@ public OperationHandlerProvider() {OperationType.Post, new RefPostOperationHandler() }, {OperationType.Delete, new RefDeleteOperationHandler() } }; + + // media entity operation (Get|Put) + _handlers[ODataPathKind.MediaEntity] = new Dictionary + { + {OperationType.Get, new MediaEntityGetOperationHandler() }, + {OperationType.Put, new MediaEntityPutOperationHandler() } + }; } /// diff --git a/src/Microsoft.OpenApi.OData.Reader/PathItem/MediaEntityPathItemHandler.cs b/src/Microsoft.OpenApi.OData.Reader/PathItem/MediaEntityPathItemHandler.cs new file mode 100644 index 00000000..24d75c5c --- /dev/null +++ b/src/Microsoft.OpenApi.OData.Reader/PathItem/MediaEntityPathItemHandler.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.OData.Edm; +using Microsoft.OpenApi.OData.Vocabulary.Capabilities; + +namespace Microsoft.OpenApi.OData.PathItem +{ + /// + /// Create a for a media entity. + /// + internal class MediaEntityPathItemHandler : EntitySetPathItemHandler + { + /// + protected override ODataPathKind HandleKind => ODataPathKind.MediaEntity; + + /// + protected override void SetOperations(OpenApiPathItem item) + { + ReadRestrictionsType read = Context.Model.GetRecord(EntitySet); + if (read == null || + (read.ReadByKeyRestrictions == null && read.IsReadable) || + (read.ReadByKeyRestrictions != null && read.ReadByKeyRestrictions.IsReadable)) + { + AddOperation(item, OperationType.Get); + } + + UpdateRestrictionsType update = Context.Model.GetRecord(EntitySet); + if (update == null || update.IsUpdatable) + { + AddOperation(item, OperationType.Put); + } + } + } +} diff --git a/src/Microsoft.OpenApi.OData.Reader/PathItem/PathItemHandlerProvider.cs b/src/Microsoft.OpenApi.OData.Reader/PathItem/PathItemHandlerProvider.cs index 380fbdcb..5cf3d570 100644 --- a/src/Microsoft.OpenApi.OData.Reader/PathItem/PathItemHandlerProvider.cs +++ b/src/Microsoft.OpenApi.OData.Reader/PathItem/PathItemHandlerProvider.cs @@ -36,6 +36,9 @@ internal class PathItemHandlerProvider : IPathItemHandlerProvider // Edm Ref { ODataPathKind.Ref, new RefPathItemHandler() }, + // Media Entity + {ODataPathKind.MediaEntity, new MediaEntityPathItemHandler() }, + // Unknown { ODataPathKind.Unknown, null }, }; diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs index e17177af..df679253 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs @@ -4,13 +4,12 @@ // ------------------------------------------------------------ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; +using System.Xml.Linq; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Csdl; -using Microsoft.OData.Edm.Validation; using Microsoft.OpenApi.OData.Tests; using Xunit; @@ -45,7 +44,7 @@ public void GetPathsForGraphBetaModelReturnsAllPaths() // Assert Assert.NotNull(paths); - Assert.Equal(4583, paths.Count()); + Assert.Equal(4585, paths.Count()); } [Fact] @@ -84,7 +83,7 @@ public void GetPathsWithSingletonWorks() public void GetPathsWithBoundFunctionOperationWorks() { // Arrange - string boundFunction = + string boundFunction = @" @@ -182,6 +181,44 @@ public void GetPathsWithNavigationPropertytWorks() Assert.Contains("/Orders({id})/MultipleCustomers/$ref", pathItems); } + [Theory] + [InlineData(true, "Logo")] + [InlineData(false, "Logo")] + [InlineData(true, "Content")] + [InlineData(false, "Content")] + public void GetPathsWithStreamPropertyAndWithEntityHasStreamWorks(bool hasStream, string streamPropName) + { + // Arrange + IEdmModel model = GetEdmModel(hasStream, streamPropName); + ODataPathProvider provider = new ODataPathProvider(); + + // Act + var paths = provider.GetPaths(model); + + // Assert + Assert.NotNull(paths); + + if (hasStream && !streamPropName.Equals("Content", StringComparison.OrdinalIgnoreCase)) + { + Assert.Equal(4, paths.Count()); + Assert.Equal(new[] { "/Todos", "/Todos({Id})", "/Todos({Id})/$value", "/Todos({Id})/Logo" }, + paths.Select(p => p.GetPathItemName())); + } + else if ((hasStream && streamPropName.Equals("Content", StringComparison.OrdinalIgnoreCase)) || + (!hasStream && streamPropName.Equals("Content", StringComparison.OrdinalIgnoreCase))) + { + Assert.Equal(3, paths.Count()); + Assert.Equal(new[] { "/Todos", "/Todos({Id})", "/Todos({Id})/Content" }, + paths.Select(p => p.GetPathItemName())); + } + else // !hasStream && !streamPropName.Equals("Content") + { + Assert.Equal(3, paths.Count()); + Assert.Equal(new[] { "/Todos", "/Todos({Id})", "/Todos({Id})/Logo" }, + paths.Select(p => p.GetPathItemName())); + } + } + private static IEdmModel GetEdmModel(string schemaElement, string containerElement) { string template = @" @@ -198,12 +235,35 @@ private static IEdmModel GetEdmModel(string schemaElement, string containerEleme {1} "; - string schema = String.Format(template, schemaElement, containerElement); - IEdmModel parsedModel; - IEnumerable errors; - bool parsed = SchemaReader.TryParse(new XmlReader[] { XmlReader.Create(new StringReader(schema)) }, out parsedModel, out errors); + string schema = string.Format(template, schemaElement, containerElement); + bool parsed = SchemaReader.TryParse(new XmlReader[] { XmlReader.Create(new StringReader(schema)) }, out IEdmModel parsedModel, out _); Assert.True(parsed); return parsedModel; } + + private static IEdmModel GetEdmModel(bool hasStream, string streamPropName) + { + string template = @" + + + + + + + + + + + + + + + +"; + string modelText = string.Format(template, hasStream, streamPropName); + bool result = CsdlReader.TryParse(XElement.Parse(modelText).CreateReader(), out IEdmModel model, out _); + Assert.True(result); + return model; + } } } diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathTests.cs index 255d95d9..57045cda 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathTests.cs @@ -105,7 +105,7 @@ public void ODataPathLastSegmentWorks() } [Fact] - public void KindPropertyReturnsUnknow() + public void KindPropertyReturnsUnknown() { // Arrange ODataKeySegment keySegment = new ODataKeySegment(_simpleKeyEntityType); @@ -198,6 +198,32 @@ public void KindPropertyReturnsOperationImport() Assert.Equal(ODataPathKind.OperationImport, path.Kind); } + [Fact] + public void KindPropertyReturnsStreamProperty() + { + // Arrange + ODataNavigationSourceSegment nsSegment = new ODataNavigationSourceSegment(_simpleKeyEntitySet); + ODataKeySegment keySegment = new ODataKeySegment(_simpleKeyEntityType); + ODataStreamPropertySegment streamPropSegment = new ODataStreamPropertySegment("Logo"); + ODataPath path = new ODataPath(nsSegment, keySegment, streamPropSegment); + + // Act & Assert + Assert.Equal(ODataPathKind.MediaEntity, path.Kind); + } + + [Fact] + public void KindPropertyReturnsStreamContent() + { + // Arrange + ODataNavigationSourceSegment nsSegment = new ODataNavigationSourceSegment(_simpleKeyEntitySet); + ODataKeySegment keySegment = new ODataKeySegment(_simpleKeyEntityType); + ODataStreamContentSegment streamContSegment = new ODataStreamContentSegment(); + ODataPath path = new ODataPath(nsSegment, keySegment, streamContSegment); + + // Act & Assert + Assert.Equal(ODataPathKind.MediaEntity, path.Kind); + } + [Theory] [InlineData(true, true, "/Orders/{Order-Id}")] [InlineData(true, false, "/Orders/{Id}")] diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamContentSegmentTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamContentSegmentTests.cs new file mode 100644 index 00000000..92439491 --- /dev/null +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamContentSegmentTests.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OpenApi.OData.Edm.Tests +{ + public class ODataStreamContentSegmentTests + { + private readonly EdmEntityType _todo; + + public ODataStreamContentSegmentTests() + { + _todo = new EdmEntityType("microsoft.graph", "Todo", + new EdmEntityType("microsoft.graph", "Task"), + isAbstract: false, + isOpen: false, + hasStream: true); + _todo.AddKeys(_todo.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String)); + _todo.AddKeys(_todo.AddStructuralProperty("Logo", EdmPrimitiveTypeKind.Stream)); + _todo.AddKeys(_todo.AddStructuralProperty("Description", EdmPrimitiveTypeKind.String)); + } + + [Fact] + public void StreamContentSegmentIdentifierPropertyReturnsCorrectDefaultValue() + { + // Arrange & Act + ODataStreamContentSegment segment = new ODataStreamContentSegment(); + + // Assert + Assert.Same("$value", segment.Identifier); + } + + [Fact] + public void KindPropertyReturnsStreamContentEnumMember() + { + // Arrange & Act + ODataStreamContentSegment segment = new ODataStreamContentSegment(); + + // Assert + Assert.Equal(ODataSegmentKind.StreamContent, segment.Kind); + } + + [Fact] + public void GetPathItemNameReturnsCorrectDefaultStreamContentValue() + { + // Arrange & Act + ODataStreamContentSegment segment = new ODataStreamContentSegment(); + + // Assert + Assert.Equal("$value", segment.GetPathItemName(new OpenApiConvertSettings())); + } + } +} diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamPropertySegmentTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamPropertySegmentTests.cs new file mode 100644 index 00000000..d5858a1b --- /dev/null +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataStreamPropertySegmentTests.cs @@ -0,0 +1,70 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Linq; +using Microsoft.OData.Edm; +using Xunit; + +namespace Microsoft.OpenApi.OData.Edm.Tests +{ + public class ODataStreamPropertySegmentTests + { + private readonly EdmEntityType _todo; + + public ODataStreamPropertySegmentTests() + { + _todo = new EdmEntityType("microsoft.graph", "Todo"); + _todo.AddKeys(_todo.AddStructuralProperty("Id", EdmPrimitiveTypeKind.String)); + _todo.AddKeys(_todo.AddStructuralProperty("Logo", EdmPrimitiveTypeKind.Stream)); + _todo.AddKeys(_todo.AddStructuralProperty("Description", EdmPrimitiveTypeKind.String)); + } + + [Fact] + public void StreamPropertySegmentConstructorThrowsArgumentNull() + { + Assert.Throws("streamPropertyName", () => new ODataStreamPropertySegment(null)); + } + + [Fact] + public void StreamPropertySegmentIdentifierPropertyReturnsStreamPropertyNameOfEntity() + { + // Arrange + var streamPropName = _todo.DeclaredStructuralProperties().First(c => c.Name == "Logo").Name; + + // Act + ODataStreamPropertySegment segment = new ODataStreamPropertySegment(streamPropName); + + // Assert + Assert.Same(streamPropName, segment.Identifier); + } + + [Fact] + public void KindPropertyReturnsStreamPropertyEnumMember() + { + // Arrange + var streamPropName = _todo.DeclaredStructuralProperties().First(c => c.Name == "Logo").Name; + + // Act + ODataStreamPropertySegment segment = new ODataStreamPropertySegment(streamPropName); + + // Assert + Assert.Equal(ODataSegmentKind.StreamProperty, segment.Kind); + } + + [Fact] + public void GetPathItemNameReturnsCorrectStreamPropertyNameOfEntity() + { + // Arrange + var streamPropName = _todo.DeclaredStructuralProperties().First(c => c.Name == "Logo").Name; + + // Act + ODataStreamPropertySegment segment = new ODataStreamPropertySegment(streamPropName); + + // Assert + Assert.Equal(streamPropName, segment.GetPathItemName(new OpenApiConvertSettings())); + } + } +} diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs new file mode 100644 index 00000000..1e9f2522 --- /dev/null +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs @@ -0,0 +1,89 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; +using Microsoft.OpenApi.OData.Edm; +using System.Linq; +using System.Xml.Linq; +using Xunit; + +namespace Microsoft.OpenApi.OData.Operation.Tests +{ + public class MediaEntityGetOperationHandlerTests + { + private readonly MediaEntityGetOperationHandler _operationalHandler = new MediaEntityGetOperationHandler(); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CreateMediaEntityGetOperationReturnsCorrectOperation(bool enableOperationId) + { + // Arrange + IEdmModel model = GetEdmModel(); + OpenApiConvertSettings settings = new OpenApiConvertSettings + { + EnableOperationId = enableOperationId + }; + + ODataContext context = new ODataContext(model, settings); + IEdmEntitySet todos = model.EntityContainer.FindEntitySet("Todos"); + Assert.NotNull(todos); + + IEdmEntityType todo = model.SchemaElements.OfType().First(c => c.Name == "Todo"); + IEdmStructuralProperty sp = todo.DeclaredStructuralProperties().First(c => c.Name == "Logo"); + ODataPath path = new ODataPath(new ODataNavigationSourceSegment(todos), + new ODataKeySegment(todos.EntityType()), + new ODataStreamPropertySegment(sp.Name)); + + // Act + var getOperation = _operationalHandler.CreateOperation(context, path); + + // Assert + Assert.NotNull(getOperation); + Assert.Equal("Get media content for Todo from Todos", getOperation.Summary); + Assert.NotNull(getOperation.Tags); + var tag = Assert.Single(getOperation.Tags); + Assert.Equal("Todos.Todo", tag.Name); + + Assert.NotNull(getOperation.Responses); + Assert.Equal(2, getOperation.Responses.Count); + Assert.Equal(new[] { "200", "default" }, getOperation.Responses.Select(r => r.Key)); + + if (enableOperationId) + { + Assert.Equal("Todos.Todo.GetLogo", getOperation.OperationId); + } + else + { + Assert.Null(getOperation.OperationId); + } + } + + public static IEdmModel GetEdmModel() + { + const string modelText = @" + + + + + + + + + + + + + + + +"; + bool result = CsdlReader.TryParse(XElement.Parse(modelText).CreateReader(), out IEdmModel model, out _); + Assert.True(result); + return model; + } + } +} diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs new file mode 100644 index 00000000..14fe92d4 --- /dev/null +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs @@ -0,0 +1,63 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using Microsoft.OData.Edm; +using Microsoft.OpenApi.OData.Edm; +using System.Linq; +using Xunit; + +namespace Microsoft.OpenApi.OData.Operation.Tests +{ + public class MediaEntityPutOperationHandlerTests + { + private readonly MediaEntityPutOperationHandler _operationalHandler = new MediaEntityPutOperationHandler(); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CreateEntityPutOperationReturnsCorrectOperation(bool enableOperationId) + { + // Arrange + IEdmModel model = MediaEntityGetOperationHandlerTests.GetEdmModel(); + OpenApiConvertSettings settings = new OpenApiConvertSettings + { + EnableOperationId = enableOperationId + }; + + ODataContext context = new ODataContext(model, settings); + IEdmEntitySet todos = model.EntityContainer.FindEntitySet("Todos"); + Assert.NotNull(todos); + + IEdmEntityType todo = model.SchemaElements.OfType().First(c => c.Name == "Todo"); + IEdmStructuralProperty sp = todo.DeclaredStructuralProperties().First(c => c.Name == "Logo"); + ODataPath path = new ODataPath(new ODataNavigationSourceSegment(todos), + new ODataKeySegment(todos.EntityType()), + new ODataStreamPropertySegment(sp.Name)); + + // Act + var getOperation = _operationalHandler.CreateOperation(context, path); + + // Assert + Assert.NotNull(getOperation); + Assert.Equal("Update media content for Todo in Todos", getOperation.Summary); + Assert.NotNull(getOperation.Tags); + var tag = Assert.Single(getOperation.Tags); + Assert.Equal("Todos.Todo", tag.Name); + + Assert.NotNull(getOperation.Responses); + Assert.Equal(2, getOperation.Responses.Count); + Assert.Equal(new[] { "204", "default" }, getOperation.Responses.Select(r => r.Key)); + + if (enableOperationId) + { + Assert.Equal("Todos.Todo.UpdateLogo", getOperation.OperationId); + } + else + { + Assert.Null(getOperation.OperationId); + } + } + } +} diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/OperationHandlerProviderTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/OperationHandlerProviderTests.cs index ca6e588b..f28668d4 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/OperationHandlerProviderTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/OperationHandlerProviderTests.cs @@ -32,6 +32,8 @@ public class OperationHandlerProviderTests [InlineData(ODataPathKind.Ref, OperationType.Delete, typeof(RefDeleteOperationHandler))] [InlineData(ODataPathKind.Ref, OperationType.Get, typeof(RefGetOperationHandler))] [InlineData(ODataPathKind.Ref, OperationType.Put, typeof(RefPutOperationHandler))] + [InlineData(ODataPathKind.MediaEntity, OperationType.Get, typeof(MediaEntityGetOperationHandler))] + [InlineData(ODataPathKind.MediaEntity, OperationType.Put, typeof(MediaEntityPutOperationHandler))] public void GetHandlerReturnsCorrectOperationHandlerType(ODataPathKind pathKind, OperationType operationType, Type handlerType) { // Arrange diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/MediaEntityPathItemHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/MediaEntityPathItemHandlerTests.cs new file mode 100644 index 00000000..53f164f4 --- /dev/null +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/MediaEntityPathItemHandlerTests.cs @@ -0,0 +1,206 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.OData.Edm; +using Microsoft.OpenApi.OData.Properties; +using System; +using System.Linq; +using System.Xml.Linq; +using Xunit; + +namespace Microsoft.OpenApi.OData.PathItem.Tests +{ + public class MediaEntityPathItemHandlerTests + { + private readonly MediaEntityPathItemHandler _pathItemHandler = new MyMediaEntityPathItemHandler(); + + [Fact] + public void CreatePathItemThrowsForNullContext() + { + // Arrange & Act & Assert + Assert.Throws("context", + () => _pathItemHandler.CreatePathItem(context: null, path: new ODataPath())); + } + + [Fact] + public void CreatePathItemThrowsForNullPath() + { + // Arrange & Act & Assert + Assert.Throws("path", + () => _pathItemHandler.CreatePathItem(new ODataContext(EdmCoreModel.Instance), path: null)); + } + + [Fact] + public void CreatePathItemThrowsForNonMediaEntityPath() + { + // Arrange + IEdmModel model = GetEdmModel(""); + ODataContext context = new ODataContext(model); + var entitySet = model.EntityContainer.FindEntitySet("Todos"); + Assert.NotNull(entitySet); // guard + var path = new ODataPath(new ODataNavigationSourceSegment(entitySet)); + + // Act + void test() => _pathItemHandler.CreatePathItem(context, path); + + // Assert + var exception = Assert.Throws(test); + Assert.Equal(string.Format(SRResource.InvalidPathKindForPathItemHandler, _pathItemHandler.GetType().Name, path.Kind), exception.Message); + } + + [Fact] + public void CreateMediaEntityPathItemReturnsCorrectItem() + { + // Arrange + IEdmModel model = GetEdmModel(""); + ODataContext context = new ODataContext(model); + var entitySet = model.EntityContainer.FindEntitySet("Todos"); + Assert.NotNull(entitySet); // guard + IEdmEntityType entityType = entitySet.EntityType(); + + IEdmStructuralProperty sp = entityType.DeclaredStructuralProperties().First(c => c.Name == "Logo"); + ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet), + new ODataKeySegment(entityType), + new ODataStreamPropertySegment(sp.Name)); + + // Act + var pathItem = _pathItemHandler.CreatePathItem(context, path); + + Assert.NotNull(pathItem.Operations); + Assert.NotEmpty(pathItem.Operations); + Assert.Equal(2, pathItem.Operations.Count); + Assert.Equal(new OperationType[] { OperationType.Get, OperationType.Put }, + pathItem.Operations.Select(o => o.Key)); + } + + [Theory] + [InlineData(true, new OperationType[] { OperationType.Get, OperationType.Put })] + [InlineData(false, new OperationType[] { OperationType.Put })] + public void CreateMediaEntityPathItemWorksForReadByKeyRestrictionsCapablities(bool readable, OperationType[] expected) + { + // Arrange + string annotation = $@" + + + + + + + + +"; + + // Assert + VerifyPathItemOperationsForStreamPropertySegment(annotation, expected); + VerifyPathItemOperationsForStreamContentSegment(annotation, expected); + } + + [Theory] + [InlineData(true, new OperationType[] { OperationType.Get, OperationType.Put })] + [InlineData(false, new OperationType[] { OperationType.Get })] + public void CreateMediaEntityPathItemWorksForUpdateRestrictionsCapablities(bool updatable, OperationType[] expected) + { + // Arrange + string annotation = $@" + + + + +"; + + // Assert + VerifyPathItemOperationsForStreamPropertySegment(annotation, expected); + VerifyPathItemOperationsForStreamContentSegment(annotation, expected); + } + + private void VerifyPathItemOperationsForStreamPropertySegment(string annotation, OperationType[] expected) + { + // Arrange + IEdmModel model = GetEdmModel(annotation); + ODataContext context = new ODataContext(model); + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Todos"); + Assert.NotNull(entitySet); // guard + IEdmEntityType entityType = entitySet.EntityType(); + + ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet), + new ODataKeySegment(entityType), + new ODataStreamContentSegment()); + + // Act + var pathItem = _pathItemHandler.CreatePathItem(context, path); + + // Assert + Assert.NotNull(pathItem); + + Assert.NotNull(pathItem.Operations); + Assert.NotEmpty(pathItem.Operations); + Assert.Equal(expected, pathItem.Operations.Select(e => e.Key)); + } + + private void VerifyPathItemOperationsForStreamContentSegment(string annotation, OperationType[] expected) + { + // Arrange + IEdmModel model = GetEdmModel(annotation); + ODataContext context = new ODataContext(model); + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Todos"); + Assert.NotNull(entitySet); // guard + IEdmEntityType entityType = entitySet.EntityType(); + + IEdmStructuralProperty sp = entityType.DeclaredStructuralProperties().First(c => c.Name == "Logo"); + ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet), + new ODataKeySegment(entityType), + new ODataStreamPropertySegment(sp.Name)); + + // Act + var pathItem = _pathItemHandler.CreatePathItem(context, path); + + // Assert + Assert.NotNull(pathItem); + + Assert.NotNull(pathItem.Operations); + Assert.NotEmpty(pathItem.Operations); + Assert.Equal(expected, pathItem.Operations.Select(e => e.Key)); + } + + private IEdmModel GetEdmModel(string annotation) + { + const string template = @" + + + + + + + + + + + + + + + {0} + + + +"; + string modelText = string.Format(template, annotation); + bool result = CsdlReader.TryParse(XElement.Parse(modelText).CreateReader(), out IEdmModel model, out _); + Assert.True(result); + return model; + } + } + + internal class MyMediaEntityPathItemHandler : MediaEntityPathItemHandler + { + protected override void AddOperation(OpenApiPathItem item, OperationType operationType) + { + item.AddOperation(operationType, new OpenApiOperation()); + } + } +} diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/PathItemHandlerProviderTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/PathItemHandlerProviderTests.cs index 4722b86a..3de9155f 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/PathItemHandlerProviderTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/PathItemHandlerProviderTests.cs @@ -19,6 +19,7 @@ public class PathItemHandlerProviderTests [InlineData(ODataPathKind.Operation, typeof(OperationPathItemHandler))] [InlineData(ODataPathKind.OperationImport, typeof(OperationImportPathItemHandler))] [InlineData(ODataPathKind.Ref, typeof(RefPathItemHandler))] + [InlineData(ODataPathKind.MediaEntity, typeof(MediaEntityPathItemHandler))] public void GetHandlerReturnsCorrectHandlerType(ODataPathKind pathKind, Type handlerType) { // Arrange