From c92c1b84c419b35ba99c8c592a6d77196319f913 Mon Sep 17 00:00:00 2001 From: Biao Li Date: Thu, 12 Jul 2018 17:34:32 -0700 Subject: [PATCH] Fix error for relative context Uri resolution with additional request uri. --- .../ODataJsonLightContextUriParser.cs | 18 +- .../JsonLight/ODataJsonLightDeserializer.cs | 7 +- .../Microsoft.OData.Core.cs | 2 +- .../Microsoft.OData.Core.txt | 2 +- .../ODataMessageReaderSettings.cs | 6 + .../Parameterized.Microsoft.OData.Core.cs | 6 +- .../Microsoft.OData.Core.Tests.csproj | 1 + .../ODataJsonLightContextUriParserTests.cs | 25 +- .../JsonLight/RelativeUriReaderTests.cs | 546 ++++++++++++++++++ .../PublicApi/PublicApi.bsl | 1 + .../ContextUriParserJsonLightTests.cs | 27 +- 11 files changed, 608 insertions(+), 33 deletions(-) create mode 100644 test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Reader/JsonLight/RelativeUriReaderTests.cs diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightContextUriParser.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightContextUriParser.cs index 5d953c8929..07e32a4b7d 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightContextUriParser.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightContextUriParser.cs @@ -61,9 +61,12 @@ private ODataJsonLightContextUriParser(IEdmModel model, Uri contextUriFromPayloa /// The model to use when resolving the target of the URI. /// The string value of the odata.metadata annotation read from the payload. /// The payload kind we expect the context URI to conform to. - /// The function of client cuetom type resolver. + /// The function of client custom type resolver. /// Whether the fragment after $metadata should be parsed, if set to false, only MetadataDocumentUri is parsed. /// Whether to throw if a type specified in the ContextUri is not found in metadata. + /// Optional value (with default value of null) of base Uri used for resolving the context url. + /// It should be a non-null value when resolving a top-level relative context Uri. + /// /// The result from parsing the context URI. internal static ODataJsonLightContextUriParseResult Parse( IEdmModel model, @@ -71,19 +74,22 @@ internal static ODataJsonLightContextUriParseResult Parse( ODataPayloadKind payloadKind, Func clientCustomTypeResolver, bool needParseFragment, - bool throwIfMetadataConflict = true) + bool throwIfMetadataConflict = true, + Uri baseUri = null) { if (contextUriFromPayload == null) { throw new ODataException(ODataErrorStrings.ODataJsonLightContextUriParser_NullMetadataDocumentUri); } - // Create an absolute URI from the payload string - // TODO: Support relative context uri and resolving other relative uris + // Create an absolute URI from the payload string. + // Uri.TryCreate(Uri, string, out Uri) try creating the Uri directly from specified string value first. + // Resultant Uri is returned directly if it is an absolute Uri; otherwise, baseUri will be used to resolve the relative Uri + // into an absolute Uri. See https://github.com/Microsoft/referencesource/blob/master/System/net/System/UriExt.cs#L315 Uri contextUri; - if (!Uri.TryCreate(contextUriFromPayload, UriKind.Absolute, out contextUri)) + if (!Uri.TryCreate(baseUri, contextUriFromPayload, out contextUri)) { - throw new ODataException(ODataErrorStrings.ODataJsonLightContextUriParser_TopLevelContextUrlShouldBeAbsolute(contextUriFromPayload)); + throw new ODataException(ODataErrorStrings.ODataJsonLightContextUriParser_TopLevelContextUrlIsInvalid(contextUriFromPayload)); } ODataJsonLightContextUriParser parser = new ODataJsonLightContextUriParser(model, contextUri); diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightDeserializer.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightDeserializer.cs index 603d6f92d0..ffff9ac774 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightDeserializer.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightDeserializer.cs @@ -255,7 +255,8 @@ internal void ReadPayloadStart( payloadKind, this.MessageReaderSettings.ClientCustomTypeResolver, this.JsonLightInputContext.ReadingResponse, - this.JsonLightInputContext.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata); + this.JsonLightInputContext.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, + this.MessageReaderSettings.RequestUri); } this.contextUriParseResult = parseResult; @@ -306,7 +307,9 @@ internal Task ReadPayloadStartAsync( contextUriAnnotationValue, payloadKind, this.MessageReaderSettings.ClientCustomTypeResolver, - this.JsonLightInputContext.ReadingResponse); + this.JsonLightInputContext.ReadingResponse, + this.JsonLightInputContext.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, + this.MessageReaderSettings.RequestUri); } #if DEBUG diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.cs b/src/Microsoft.OData.Core/Microsoft.OData.Core.cs index 7d640b56b5..31be025b92 100644 --- a/src/Microsoft.OData.Core/Microsoft.OData.Core.cs +++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.cs @@ -513,7 +513,7 @@ internal sealed class TextRes { internal const string ODataJsonLightContextUriParser_NoModel = "ODataJsonLightContextUriParser_NoModel"; internal const string ODataJsonLightContextUriParser_InvalidContextUrl = "ODataJsonLightContextUriParser_InvalidContextUrl"; internal const string ODataJsonLightContextUriParser_LastSegmentIsKeySegment = "ODataJsonLightContextUriParser_LastSegmentIsKeySegment"; - internal const string ODataJsonLightContextUriParser_TopLevelContextUrlShouldBeAbsolute = "ODataJsonLightContextUriParser_TopLevelContextUrlShouldBeAbsolute"; + internal const string ODataJsonLightContextUriParser_TopLevelContextUrlIsInvalid = "ODataJsonLightContextUriParser_TopLevelContextUrlIsInvalid"; internal const string ODataJsonLightResourceDeserializer_DeltaRemovedAnnotationMustBeObject = "ODataJsonLightResourceDeserializer_DeltaRemovedAnnotationMustBeObject"; internal const string ODataJsonLightResourceDeserializer_ResourceTypeAnnotationNotFirst = "ODataJsonLightResourceDeserializer_ResourceTypeAnnotationNotFirst"; internal const string ODataJsonLightResourceDeserializer_ResourceInstanceAnnotationPrecededByProperty = "ODataJsonLightResourceDeserializer_ResourceInstanceAnnotationPrecededByProperty"; diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt index 459cec5db7..ac785390db 100644 --- a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt +++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt @@ -541,7 +541,7 @@ ODataJsonLightContextUriParser_InvalidPayloadKindWithSelectQueryOption=A '$selec ODataJsonLightContextUriParser_NoModel=No model was specified for the ODataMessageReader. A message reader requires a model for JSON Light payload to be specified in the ODataMessageReader constructor. ODataJsonLightContextUriParser_InvalidContextUrl=The context URL '{0}' is invalid. ODataJsonLightContextUriParser_LastSegmentIsKeySegment=Last segment in context URL '{0}' should not be KeySegment. -ODataJsonLightContextUriParser_TopLevelContextUrlShouldBeAbsolute=The top level context URL '{0}' should be an absolute Uri. +ODataJsonLightContextUriParser_TopLevelContextUrlIsInvalid=The top level context URL '{0}' is not a valid absolute or relative Uri. ODataJsonLightResourceDeserializer_DeltaRemovedAnnotationMustBeObject=Invalid primitive value '{0}' for @removed annotation. @removed annotation must be a JSON object, optionally containing a 'reason' property. ODataJsonLightResourceDeserializer_ResourceTypeAnnotationNotFirst=The 'odata.type' instance annotation in a resource object is preceded by an invalid property. In OData, the 'odata.type' instance annotation must be either the first property in the JSON object or the second if the 'odata.context' instance annotation is present. diff --git a/src/Microsoft.OData.Core/ODataMessageReaderSettings.cs b/src/Microsoft.OData.Core/ODataMessageReaderSettings.cs index b168c674ad..f415d684f1 100644 --- a/src/Microsoft.OData.Core/ODataMessageReaderSettings.cs +++ b/src/Microsoft.OData.Core/ODataMessageReaderSettings.cs @@ -176,6 +176,11 @@ public ODataMessageQuotas MessageQuotas /// public bool ReadUntypedAsString { get; set; } + /// + /// Gets or sets current request Uri. + /// + public Uri RequestUri { get; set; } + /// /// Func to evaluate whether an annotation should be read or skipped by the reader. The func should return true if the annotation should /// be read and false if the annotation should be skipped. A null value indicates that all annotations should be skipped. @@ -262,6 +267,7 @@ private void CopyFrom(ODataMessageReaderSettings other) this.validations = other.validations; this.ThrowOnDuplicatePropertyNames = other.ThrowOnDuplicatePropertyNames; this.ThrowIfTypeConflictsWithMetadata = other.ThrowIfTypeConflictsWithMetadata; + this.RequestUri = other.RequestUri; this.ThrowOnUndeclaredPropertyForNonOpenType = other.ThrowOnUndeclaredPropertyForNonOpenType; this.LibraryCompatibility = other.LibraryCompatibility; this.Version = other.Version; diff --git a/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs b/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs index cb2eba132b..db5cb00567 100644 --- a/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs +++ b/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs @@ -3495,10 +3495,10 @@ internal static string ODataJsonLightContextUriParser_LastSegmentIsKeySegment(ob } /// - /// A string like "The top level context URL '{0}' should be an absolute Uri." + /// A string like "The top level context URL '{0}' is not a valid absolute or relative Uri." /// - internal static string ODataJsonLightContextUriParser_TopLevelContextUrlShouldBeAbsolute(object p0) { - return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.ODataJsonLightContextUriParser_TopLevelContextUrlShouldBeAbsolute, p0); + internal static string ODataJsonLightContextUriParser_TopLevelContextUrlIsInvalid(object p0) { + return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.ODataJsonLightContextUriParser_TopLevelContextUrlIsInvalid, p0); } /// diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/Build.NetFramework/Microsoft.OData.Core.Tests.csproj b/test/FunctionalTests/Microsoft.OData.Core.Tests/Build.NetFramework/Microsoft.OData.Core.Tests.csproj index 4407bd8f10..875ea43e47 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/Build.NetFramework/Microsoft.OData.Core.Tests.csproj +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/Build.NetFramework/Microsoft.OData.Core.Tests.csproj @@ -225,6 +225,7 @@ + diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightContextUriParserTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightContextUriParserTests.cs index ecf77e5c1b..cb3d72fc51 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightContextUriParserTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightContextUriParserTests.cs @@ -30,13 +30,32 @@ private EdmModel GetModel() return model; } - // TODO: Support relative context uri and resolving other relative uris [Fact] - public void ParseRelativeContextUrlShouldThrowException() + public void ParseRelativeContextUrlWithBaseUrl() + { + const bool needParseSegment = true; + const bool throwIfMetadataConflict = true; + + var model = new EdmModel(); + var entityType = new EdmEntityType("Sample", "R"); + model.AddElement(entityType); + string relativeUrl1 = "$metadata#Sample.R"; + string relativeUrl2 = "/SampleService/$metadata#Sample.R"; + var parseResult = ODataJsonLightContextUriParser.Parse(model, relativeUrl1, ODataPayloadKind.Unsupported, null, needParseSegment, + throwIfMetadataConflict, new Uri("http://service/SampleService/EntitySet")); + parseResult.ContextUri.OriginalString.Should().Be("http://service/SampleService/$metadata#Sample.R"); + + parseResult = ODataJsonLightContextUriParser.Parse(model, relativeUrl2, ODataPayloadKind.Unsupported, null, needParseSegment, + throwIfMetadataConflict, new Uri("http://service/SampleService/EntitySet")); + parseResult.ContextUri.OriginalString.Should().Be("http://service/SampleService/$metadata#Sample.R"); + } + + [Fact] + public void ParseRelativeContextUrlWithoutBaseUriShouldThrowException() { string relativeUrl = "$metadata#R"; Action parseContextUri = () => ODataJsonLightContextUriParser.Parse(new EdmModel(), relativeUrl, ODataPayloadKind.Unsupported, null, true); - parseContextUri.ShouldThrow().WithMessage(ErrorStrings.ODataJsonLightContextUriParser_TopLevelContextUrlShouldBeAbsolute(relativeUrl)); + parseContextUri.ShouldThrow().WithMessage(ErrorStrings.ODataJsonLightContextUriParser_TopLevelContextUrlIsInvalid(relativeUrl)); } [Fact] diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Reader/JsonLight/RelativeUriReaderTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Reader/JsonLight/RelativeUriReaderTests.cs new file mode 100644 index 0000000000..19a9c1d7db --- /dev/null +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Reader/JsonLight/RelativeUriReaderTests.cs @@ -0,0 +1,546 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.OData.UriParser; +using Microsoft.OData.Edm; +using Microsoft.OData.Tests; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OData.Core.Tests.ScenarioTests.Reader.JsonLight +{ + public class RelativeUriReaderTests + { + private readonly ITestOutputHelper output; + + private const string NS = "SampleService"; + private const string ServiceRoot = "http://www.example.com/SampleService/"; + private static readonly Uri ServiceDocumentUri = new Uri(ServiceRoot); + + #region variable + + private EdmModel model; + private EdmEntityType et1; + private EdmEntityType derivedET1; + private EdmComplexType ct1; + private EdmComplexType derivedCT1; + private EdmEntityType et2; + private EdmEntityType et3; + + private EdmEntitySet et1Set; + private EdmEntitySet et2Set; + private EdmSingleton et2Singleton; + + private EdmNavigationProperty et1CNav1; + private EdmNavigationProperty et1CNav2; + + #endregion variable + + public RelativeUriReaderTests(ITestOutputHelper output) + { + this.output = output; + + #region GenerateModel + + model = new EdmModel(); + + ct1 = new EdmComplexType(NS, "CT1"); + ct1.AddStructuralProperty("CT1P1", EdmCoreModel.Instance.GetString(false)); + model.AddElement(ct1); + + derivedCT1 = new EdmComplexType(NS, "DerivedCT1", ct1); + derivedCT1.AddStructuralProperty("DerivedCT1P1", EdmCoreModel.Instance.GetInt32(true)); + model.AddElement(derivedCT1); + + et1 = new EdmEntityType(NS, "ET1"); + var et1key = new EdmStructuralProperty(et1, "ET1Key", EdmCoreModel.Instance.GetInt32(false)); + et1.AddProperty(et1key); + et1.AddKeys(et1key); + et1.AddStructuralProperty("ET1P1", EdmCoreModel.Instance.GetString(false)); + model.AddElement(et1); + + derivedET1 = new EdmEntityType(NS, "DerivedET1", et1); + derivedET1.AddStructuralProperty("DerivedET1P1", EdmCoreModel.Instance.GetInt32(true)); + model.AddElement(derivedET1); + + et2 = new EdmEntityType(NS, "ET2"); + var et2key = new EdmStructuralProperty(et2, "ET2Key", EdmCoreModel.Instance.GetInt32(false)); + et2.AddProperty(et2key); + et2.AddKeys(et2key); + et2.AddStructuralProperty("ET2P1", EdmCoreModel.Instance.GetString(false)); + model.AddElement(et2); + + et3 = new EdmEntityType(NS, "ET3"); + var et3key = new EdmStructuralProperty(et3, "ET3Key", EdmCoreModel.Instance.GetInt32(false)); + et3.AddProperty(et3key); + et3.AddKeys(et3key); + et3.AddStructuralProperty("ET3P1", EdmCoreModel.Instance.GetString(false)); + model.AddElement(et3); + + var container = new EdmEntityContainer(NS, "SampleService"); + this.model.AddElement(container); + + et1Set = new EdmEntitySet(container, "ET1Set", et1); + container.AddElement(et1Set); + + et2Set = new EdmEntitySet(container, "ET2Set", et2); + container.AddElement(et2Set); + + et2Singleton = new EdmSingleton(container, "ET2Singleton", et2); + container.AddElement(et2Singleton); + + var et1Nav1Info = new EdmNavigationPropertyInfo() + { + Name = "ET1Nav1", + ContainsTarget = false, + Target = et2, + TargetMultiplicity = Edm.EdmMultiplicity.Many + }; + + var et1Nav1 = et1.AddUnidirectionalNavigation(et1Nav1Info); + et1Set.AddNavigationTarget(et1Nav1, et2Set); + + var et1Nav2Info = new EdmNavigationPropertyInfo() + { + Name = "ET1Nav2", + ContainsTarget = false, + Target = et2, + TargetMultiplicity = Edm.EdmMultiplicity.One + }; + + var et1Nav2 = et1.AddUnidirectionalNavigation(et1Nav2Info); + et1Set.AddNavigationTarget(et1Nav2, et2Singleton); + + var et1CNav1Info = new EdmNavigationPropertyInfo() + { + Name = "ET1CNav1", + ContainsTarget = true, + Target = et3, + TargetMultiplicity = Edm.EdmMultiplicity.Many + }; + + et1CNav1 = et1.AddUnidirectionalNavigation(et1CNav1Info); + + var et1CNav2Info = new EdmNavigationPropertyInfo() + { + Name = "ET1CNav2", + ContainsTarget = true, + Target = et3, + TargetMultiplicity = Edm.EdmMultiplicity.One + }; + + et1CNav2 = et1.AddUnidirectionalNavigation(et1CNav2Info); + + #endregion + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public void RelativeContextUri_ServiceDocument_RelativeUrlForEntitySet(bool startWithSlash, bool relativeUriForEntitySet) + { + string contentType; + + var payload = this.WritePayload(this.model, omWriter => + { + ODataServiceDocument serviceDocument = new ODataServiceDocument() + { + EntitySets = new[] { new ODataEntitySetInfo { Name = "ET1Set", Url = new Uri(ServiceDocumentUri, "ET1Set") } } + }; + omWriter.WriteServiceDocument(serviceDocument); + }, null, out contentType); + + // relativeUriForEntitySet == true means that the url for entity set in payload will also be changed to relative uri + string FixReplacementFormat = relativeUriForEntitySet ? "{0}" : "@odata.context\":\"{0}"; + payload = startWithSlash + ? payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "/SampleService/")) + : payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "")); + + output.WriteLine(payload); + + ODataServiceDocument serviceDocumentResult = null; + this.ReadPayload(payload, contentType, model, omReader => serviceDocumentResult = omReader.ReadServiceDocument(), ServiceDocumentUri); + + var entitySets = serviceDocumentResult.EntitySets.ToList(); + Assert.Equal(1, entitySets.Count); + Assert.Equal(new Uri(ServiceDocumentUri, "ET1Set"), entitySets[0].Url); + } + + // Test ReadPayloadStart with request Uri and relative Uri + [Theory] + [InlineData(true)] + [InlineData(false)] + public void RelativeContextUri_Feed(bool startWithSlash) + { + string contentType; + + var payload = this.WritePayload(this.model, omWriter => + { + var odataWriter = omWriter.CreateODataResourceSetWriter(et1Set, et1); + + ODataResourceSet feed = new ODataResourceSet(); + ODataResource resource = CreateResource(1); + + odataWriter.WriteStart(feed); + odataWriter.WriteStart(resource); + odataWriter.WriteEnd(); + odataWriter.WriteEnd(); + + }, null, out contentType); + + string FixReplacementFormat = "@odata.context\":\"{0}"; + payload = startWithSlash + ? payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "/SampleService/")) + : payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "")); + + output.WriteLine(payload); + + this.ReadPayload(payload, contentType, model, omReader => + { + var odataReader = omReader.CreateODataResourceSetReader(); + while (odataReader.Read()) + { + if (odataReader.State == ODataReaderState.ResourceSetEnd) + { + Assert.NotNull(odataReader.Item as ODataResourceSet); + } + } + }, new Uri(ServiceDocumentUri, et1Set.Name)); + } + + // Test ReadPayloadStart with request Uri and relative Uri + [Theory] + [InlineData(true)] + [InlineData(false)] + public void RelativeContextUri_Entry_Expand(bool startWithSlash) + { + var payload = @" +{ + ""@odata.context"":""http://www.example.com/SampleService/$metadata#ET1Set/$entity"", + ""ET1Key"":1, + ""ET1P1"":""P1"", + ""ET1Nav1"": + [ + { + ""@odata.context"":""http://www.example.com/SampleService/$metadata#ET1Set(1)/ET1Nav1/$entity"", + ""ET2Key"":1, + ""ET2P1"":""P1"" + } + ] +}"; + + string FixReplacementFormat = "@odata.context\":\"{0}"; + payload = startWithSlash + ? payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "/SampleService/")) + : payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "")); + + output.WriteLine(payload); + + this.ReadPayload(payload, "application/json", model, omReader => + { + var odataReader = omReader.CreateODataResourceReader(); + while (odataReader.Read()) + { + if (odataReader.State == ODataReaderState.ResourceEnd) + { + Assert.NotNull(odataReader.Item as ODataResource); + } + } + }, new Uri(ServiceDocumentUri, "ET1Set(1)?$expand=ET1Nav1")); + } + + // Test StartNavigationLink with request Uri + [Theory] + [InlineData(true)] + [InlineData(false)] + public void RelativeUri_ContainedNavigation(bool startWithSlash) + { + string contentType = "application/json"; + + Uri requestUri = new Uri(ServiceDocumentUri, et1Set.Name + "(1)"); + + var payload = this.WritePayload(this.model, omWriter => + { + var odataWriter = omWriter.CreateODataResourceWriter(et1Set, et1); + + ODataResource et1Entry = CreateResource(1); + ODataNestedResourceInfo navigationLink = new ODataNestedResourceInfo() + { + IsCollection = true, + Name = "ET1CNav1", + }; + + ODataResourceSet feed = new ODataResourceSet(); + ODataResource et3Entry = CreateResource(3); + + odataWriter.WriteStart(et1Entry); + odataWriter.WriteStart(navigationLink); + odataWriter.WriteStart(feed); + odataWriter.WriteStart(et3Entry); + odataWriter.WriteEnd(); // end et3Entry + odataWriter.WriteEnd(); // end feed + odataWriter.WriteEnd(); // end navigationLink + odataWriter.WriteEnd(); // end et1Entry + + }, requestUri, out contentType); + + string FixReplacementFormat = "@odata.context\":\"{0}"; + payload = startWithSlash + ? payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "/SampleService/")) + : payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "")); + + output.WriteLine(payload); + + this.ReadPayload(payload, contentType, model, omReader => + { + var odataReader = omReader.CreateODataResourceReader(); + while (odataReader.Read()) + { + if (odataReader.State == ODataReaderState.NestedResourceInfoEnd) + { + var navLink = (odataReader.Item as ODataNestedResourceInfo); + if (navLink != null && navLink.Name.Equals("ET1CNav1")) + { + Assert.Equal(new Uri(requestUri, "ET1Set(1)/ET1CNav1").OriginalString, navLink.Url.OriginalString); + } + } + } + }, requestUri); + } + + // Test StartNavigationLink with request Uri and single navigation link. + [Theory] + [InlineData(true)] + [InlineData(false)] + public void RelativeUri_ContainedNavigation_SingleNav(bool startWithSlash) + { + string contentType; + + Uri requestUri = new Uri(ServiceDocumentUri, et1Set.Name + "(1)"); + + var payload = this.WritePayload(this.model, omWriter => + { + var odataWriter = omWriter.CreateODataResourceWriter(et1Set, et1); + + ODataResource entry = CreateResource(1); + + ODataNestedResourceInfo navigationLink = new ODataNestedResourceInfo() + { + IsCollection = false, + Name = "ET1CNav2", + }; + + ODataResource et3Entry = CreateResource(3); + + odataWriter.WriteStart(entry); + odataWriter.WriteStart(navigationLink); + odataWriter.WriteStart(et3Entry); + odataWriter.WriteEnd(); // end et3Entry + odataWriter.WriteEnd(); // end navigationLink + odataWriter.WriteEnd(); // end entry + + }, requestUri, out contentType); + + string FixReplacementFormat = "@odata.context\":\"{0}"; + payload = startWithSlash + ? payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "/SampleService/")) + : payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "")); + + output.WriteLine(payload); + + this.ReadPayload(payload, contentType, model, omReader => + { + var odataReader = omReader.CreateODataResourceReader(); + while (odataReader.Read()) + { + if (odataReader.State == ODataReaderState.NestedResourceInfoEnd) + { + var navLink = (odataReader.Item as ODataNestedResourceInfo); + if (navLink != null && navLink.Name.Equals("ET1CNav2")) + { + Assert.Equal(new Uri(requestUri, "ET1Set(1)/ET1CNav2").OriginalString, + navLink.Url.OriginalString); + } + } + } + }, requestUri); + } + + // Test ReadDeltaStart with request Uri and relative Context Uri + [Theory] + [InlineData(true)] + [InlineData(false)] + public void RelativeContextUri_Delta_ContainedNavigation(bool startWithSlash) + { + string contentType; + + Uri requestUri = new Uri(ServiceDocumentUri, et1Set.Name + "(1)"); + + var payload = this.WritePayload(this.model, omWriter => + { + var deltaWriter = omWriter.CreateODataDeltaWriter(et1Set, et1); + + var deltaFeed = new ODataDeltaResourceSet(); + ODataResource deltaEntry = CreateResource(1, new Uri(ServiceDocumentUri, "ET1Set(1)")); + + var deletedEntry = new ODataDeltaDeletedEntry( + new Uri(ServiceDocumentUri, "ET1Set(2)/ET1CNav1(1)").AbsoluteUri, + DeltaDeletedEntryReason.Deleted); + deletedEntry.SetSerializationInfo(new ODataDeltaSerializationInfo() + { + NavigationSourceName = "ET1Set(2)/ET1CNav1" + }); + + var deletedLink = new ODataDeltaDeletedLink( + new Uri(ServiceDocumentUri, "ET1Set(2))"), + new Uri(ServiceDocumentUri, "ET1Set(2)/ET1CNav1(1)"), + "ET1CNav1"); + + ODataNestedResourceInfo navigationLink = new ODataNestedResourceInfo() + { + IsCollection = false, + Name = "ET1CNav1", + }; + + ODataResource et3Entry = CreateResource(3, new Uri(ServiceDocumentUri, "ET1Set(2)/ET1CNav1(1)")); + + var addedLink = new ODataDeltaLink( + new Uri(ServiceDocumentUri, "ET1Set(2)"), + new Uri(ServiceDocumentUri, "ET1Set(2)/ET1CNav1(1)"), + "ET1CNav1"); + + et3Entry.SetSerializationInfo(new ODataResourceSerializationInfo + { + NavigationSourceEntityTypeName = NS + "." + et3.Name, + NavigationSourceKind = EdmNavigationSourceKind.ContainedEntitySet, + NavigationSourceName = "ET1Set(2)/ET1CNav1" + }); + + ODataNestedResourceInfo nav2 = new ODataNestedResourceInfo() + { + IsCollection = true, + Name = "ET1Nav1", + }; + + ODataResourceSet nav2Feed = new ODataResourceSet(); + ODataResource et2Entry = CreateResource(2, new Uri(ServiceDocumentUri, "ET1Set(2)/ET1Nav1(1)")); + + deltaWriter.WriteStart(deltaFeed); + deltaWriter.WriteStart(deltaEntry); + + deltaWriter.WriteStart(nav2); + deltaWriter.WriteStart(nav2Feed); + deltaWriter.WriteStart(et2Entry); + deltaWriter.WriteEnd(); + deltaWriter.WriteEnd(); + deltaWriter.WriteEnd(); + + deltaWriter.WriteEnd(); + deltaWriter.WriteDeltaDeletedEntry(deletedEntry); + deltaWriter.WriteDeltaDeletedLink(deletedLink); + deltaWriter.WriteStart(et3Entry); + deltaWriter.WriteEnd(); + deltaWriter.WriteDeltaLink(addedLink); + + deltaWriter.WriteEnd(); + + }, requestUri, out contentType); + + string FixReplacementFormat = "@odata.context\":\"{0}"; + payload = startWithSlash + ? payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "/SampleService/")) + : payload.Replace(string.Format(FixReplacementFormat, ServiceRoot), string.Format(FixReplacementFormat, "")); + + output.WriteLine(payload); + + this.ReadPayload(payload, "application/json", model, omReader => + { + var deltaReader = omReader.CreateODataDeltaReader(et1Set, et1); + while (deltaReader.Read()) ; + }, requestUri); + } + + private string WritePayload(EdmModel edmModel, Action write, Uri requestUri, out string contentType) + { + var message = new InMemoryMessage() { Stream = new MemoryStream() }; + + var writerSettings = new ODataMessageWriterSettings + { + EnableMessageStreamDisposal = false + }; + + writerSettings.SetServiceDocumentUri(ServiceDocumentUri); + writerSettings.SetContentType(ODataFormat.Json); + + if (requestUri != null) + { + ODataUriParser odataUriParser = new ODataUriParser(edmModel, ServiceDocumentUri, requestUri); + writerSettings.ODataUri = new ODataUri() + { + ServiceRoot = ServiceDocumentUri, + Path = odataUriParser.ParsePath() + }; + } + + using (var msgWriter = new ODataMessageWriter((IODataResponseMessage)message, writerSettings, edmModel)) + { + write(msgWriter); + } + + message.Stream.Seek(0, SeekOrigin.Begin); + using (StreamReader reader = new StreamReader(message.Stream)) + { + contentType = message.GetHeader("Content-Type"); + return reader.ReadToEnd(); + } + } + + private void ReadPayload(string payload, string contentType, EdmModel edmModel, Action test, Uri requestUri) + { + var message = new InMemoryMessage() { Stream = new MemoryStream(Encoding.UTF8.GetBytes(payload)) }; + message.SetHeader("Content-Type", contentType); + + ODataMessageReaderSettings readerSettings = new ODataMessageReaderSettings() + { + BaseUri = new Uri(ServiceDocumentUri, "$metadata"), + RequestUri = requestUri + }; + + using (var msgReader = new ODataMessageReader((IODataResponseMessage)message, readerSettings, edmModel)) + { + test(msgReader); + } + } + + private static ODataResource CreateResource(int seq, Uri id = null) + { + return new ODataResource() + { + Id = id, + Properties = new List + { + new ODataProperty() + { + Name = $"ET{seq}Key", + Value = 1, + }, + new ODataProperty() + { + Name = string.Format("ET{0}P1", seq), + Value = "P1", + } + } + }; + } + } +} \ No newline at end of file diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl index 4a9a31fddd..d292c96da3 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl @@ -5094,6 +5094,7 @@ public sealed class Microsoft.OData.ODataMessageReaderSettings { Microsoft.OData.ODataMessageQuotas MessageQuotas { public get; public set; } System.Func`3[[System.Object],[System.String],[Microsoft.OData.Edm.IEdmTypeReference]] PrimitiveTypeResolver { public get; public set; } bool ReadUntypedAsString { public get; public set; } + System.Uri RequestUri { public get; public set; } System.Func`2[[System.String],[System.Boolean]] ShouldIncludeAnnotation { public get; public set; } Microsoft.OData.ValidationKinds Validations { public get; public set; } System.Nullable`1[[Microsoft.OData.ODataVersion]] Version { public get; public set; } diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Reader.Tests/JsonLight/ContextUriParserJsonLightTests.cs b/test/FunctionalTests/Tests/DataOData/Tests/OData.Reader.Tests/JsonLight/ContextUriParserJsonLightTests.cs index ac156e4418..31bee03b01 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Reader.Tests/JsonLight/ContextUriParserJsonLightTests.cs +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Reader.Tests/JsonLight/ContextUriParserJsonLightTests.cs @@ -89,17 +89,10 @@ public void JsonLightContextUriParserErrorTest() }, new ContextUriParserTestCase { - DebugDescription = "empty string is a relative URI; context URIs have to absolute", + DebugDescription = "empty string is a relative URI; context URIs have to be absolute or relative", ContextUri = string.Empty, Model = this.testModel, - ExpectedException = ODataExpectedExceptions.ODataException("ODataJsonLightContextUriParser_TopLevelContextUrlShouldBeAbsolute", "") - }, - new ContextUriParserTestCase - { - DebugDescription = "another relative URI; context URIs have to absolute", - ContextUri = "relativeUri", - Model = this.testModel, - ExpectedException = ODataExpectedExceptions.ODataException("ODataJsonLightContextUriParser_TopLevelContextUrlShouldBeAbsolute", "relativeUri") + ExpectedException = ODataExpectedExceptions.ODataException("ODataJsonLightContextUriParser_TopLevelContextUrlIsInvalid", "") }, new ContextUriParserTestCase { @@ -470,7 +463,7 @@ public void JsonLightContextUriParserEntryTest() PayloadKind = ODataPayloadKind.Resource, ContextUri = MetadataDocumentUri + "#NonExistingContainer.Persons/TestModel.Employee/$entity", ExpectedException = ODataExpectedExceptions.ODataException( - "ODataJsonLightContextUriParser_InvalidContextUrl", + "ODataJsonLightContextUriParser_InvalidContextUrl", MetadataDocumentUri + "#NonExistingContainer.Persons/TestModel.Employee/$entity") }, // Metadata document URI for an entry with a type cast to a non-existing entity type @@ -479,7 +472,7 @@ public void JsonLightContextUriParserEntryTest() PayloadKind = ODataPayloadKind.Resource, ContextUri = MetadataDocumentUri + "#TestModel.DefaultContainer.Persons/TestModel.NonExistingType/$entity", ExpectedException = ODataExpectedExceptions.ODataException( - "ODataJsonLightContextUriParser_InvalidContextUrl", + "ODataJsonLightContextUriParser_InvalidContextUrl", MetadataDocumentUri + "#TestModel.DefaultContainer.Persons/TestModel.NonExistingType/$entity") }, // Metadata document URI for an entry of a type incompatible with the base type of the entity set @@ -515,7 +508,7 @@ public void JsonLightContextUriParserEntryTest() PayloadKind = ODataPayloadKind.ResourceSet, ContextUri = MetadataDocumentUri + "#TestModel.DefaultContainer.WrongSet/$entity", ExpectedException = ODataExpectedExceptions.ODataException( - "ODataJsonLightContextUriParser_InvalidContextUrl", + "ODataJsonLightContextUriParser_InvalidContextUrl", MetadataDocumentUri + "#TestModel.DefaultContainer.WrongSet/$entity") }, // Metadata document URI for an entry with type cast (invalid entity set) @@ -524,7 +517,7 @@ public void JsonLightContextUriParserEntryTest() PayloadKind = ODataPayloadKind.ResourceSet, ContextUri = MetadataDocumentUri + "#DefaultContainer.WrongSet/TestModel.Employee/$entity", ExpectedException = ODataExpectedExceptions.ODataException( - "ODataJsonLightContextUriParser_InvalidContextUrl", + "ODataJsonLightContextUriParser_InvalidContextUrl", MetadataDocumentUri + "#DefaultContainer.WrongSet/TestModel.Employee/$entity") }, #endregion Cases @@ -653,7 +646,7 @@ public void JsonLightContextUriParserFeedTest() ContextUri = MetadataDocumentUri + "#TestModel.DefaultContainer.WrongSet", ExpectedException = ODataExpectedExceptions.ODataException( "ODataJsonLightContextUriParser_InvalidEntitySetNameOrTypeName", - MetadataDocumentUri + "#TestModel.DefaultContainer.WrongSet", + MetadataDocumentUri + "#TestModel.DefaultContainer.WrongSet", "TestModel.DefaultContainer.WrongSet") }, // Metadata document URI for a feed with a type cast (invalid entity set) @@ -662,7 +655,7 @@ public void JsonLightContextUriParserFeedTest() PayloadKind = ODataPayloadKind.ResourceSet, ContextUri = MetadataDocumentUri + "#DefaultContainer.WrongSet/TestModel.Employee", ExpectedException = ODataExpectedExceptions.ODataException( - "ODataJsonLightContextUriParser_InvalidContextUrl", + "ODataJsonLightContextUriParser_InvalidContextUrl", MetadataDocumentUri + "#DefaultContainer.WrongSet/TestModel.Employee") }, #endregion Cases @@ -753,7 +746,7 @@ public void JsonLightContextUriParserDeltaTest() public void JsonLightContextUriParserSelectQueryOptionTest() { const string contextUriSuffix = "#TestModel.DefaultContainer.Persons"; - // NOTE: testing of the parsing of the $select query option itself is already done + // NOTE: testing of the parsing of the $select query option itself is already done // in the TDD tests (see ProjectedPropertiesHierarchyAnnotationTests.cs) var testCases = new ContextUriParserTestCase[] { @@ -837,7 +830,7 @@ public void JsonLightContextUriParserSelectQueryOptionTest() // SelectQueryOption = "Id,*", // }, // ExpectedException = ODataExpectedExceptions.ODataException( - // "ODataJsonLightContextUriParser_InvalidPayloadKindWithSelectQueryOption", + // "ODataJsonLightContextUriParser_InvalidPayloadKindWithSelectQueryOption", // "Property") //}, #endregion Cases