Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
using Microsoft.SharePoint.Client;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using PnP.Framework.Modernization.Cache;
using PnP.Framework.Modernization.Entities;
using PnP.Framework.Modernization.Functions;
using PnP.Framework.Modernization.Telemetry;
using PnP.Framework.Modernization.Transform;
using PnP.Framework.Utilities.UnitTests.Model;
using PnP.Framework.Utilities.UnitTests.Web;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Xml.Serialization;

namespace PnP.Framework.Modernization.Test.Transform.Functions
{
[TestClass]
public class BuiltInDocumentEmbedLookupTests
{
private const string Host = "https://contoso.sharepoint.com";
private const string SiteUrl = Host + "/sites/document-embed-tests";
private const string WebServerRelativeUrl = "/sites/document-embed-tests";
private const string DocumentUrl = WebServerRelativeUrl + "/Documents/missing.docx";
private const string OtherWebDocumentUrl = "/sites/other/Documents/missing.docx";
private static readonly Guid DocumentListId = Guid.Parse("f266cd97-f155-4360-b802-47bb9f0d9b55");
private static readonly Guid DocumentUniqueId = Guid.Parse("a9b5b53e-2014-47d9-b26c-fc8d52047d1e");
private static readonly string[] MetadataKeys =
{
"DocumentWeb", "DocumentListId", "DocumentUniqueId", "DocumentAuthor", "DocumentAuthorName"
};

[DataTestMethod]
[DataRow(null)]
[DataRow("")]
public void DocumentEmbedLookup_WhenPathIsEmpty_ReturnsCompleteEmptyMetadata(string path)
{
using (var context = CreateContext())
{
AssertEmptyMetadata(CreateBuiltIn(context).DocumentEmbedLookup(path));
}
}

[TestMethod]
public void DocumentEmbedLookup_WhenFileExists_ReturnsDocumentMetadata()
{
using (var context = CreateContext())
{
var result = CreateBuiltIn(context).DocumentEmbedLookup(DocumentUrl);

Assert.AreEqual(WebServerRelativeUrl, result["DocumentWeb"]);
Assert.AreEqual(DocumentListId.ToString(), result["DocumentListId"]);
Assert.AreEqual(DocumentUniqueId.ToString(), result["DocumentUniqueId"]);
Assert.AreEqual("author@contoso.com", result["DocumentAuthor"]);
Assert.AreEqual("Document Author", result["DocumentAuthorName"]);
}
}

[DataTestMethod]
[DataRow(DocumentUrl)]
[DataRow(OtherWebDocumentUrl)]
public void DocumentEmbedLookup_WhenFileIsNotFound_ReturnsMetadataAndLogsReference(string path)
{
using (var context = CreateContext())
{
var observer = new Mock<ILogObserver>();
var builtIn = CreateBuiltIn(context, observer.Object);
SetErrorResponse(context, "System.IO.FileNotFoundException", "File Not Found.", -2147024894);

AssertEmptyMetadata(builtIn.DocumentEmbedLookup(path));

observer.Verify(log => log.Warning(It.Is<LogEntry>(entry =>
entry.Message.Contains(path) && entry.Message.Contains(SiteUrl) &&
entry.Message.Contains("File Not Found") && entry.Message.Contains("access"))), Times.Once);
observer.Verify(log => log.Error(It.IsAny<LogEntry>()), Times.Never);
}
}

[DataTestMethod]
[DataRow("System.UnauthorizedAccessException", "Access denied.", -2147024891)]
[DataRow("Microsoft.SharePoint.SPException", "Unexpected server error.", -1)]
public void DocumentEmbedLookup_WhenAnotherServerErrorOccurs_LogsAndRethrows(string type, string message, int code)
{
using (var context = CreateContext())
{
var observer = new Mock<ILogObserver>();
var builtIn = CreateBuiltIn(context, observer.Object);
SetErrorResponse(context, type, message, code);

ServerException exception = type == "System.UnauthorizedAccessException"
? Assert.ThrowsException<ServerUnauthorizedAccessException>(() => builtIn.DocumentEmbedLookup(DocumentUrl))
: Assert.ThrowsException<ServerException>(() => builtIn.DocumentEmbedLookup(DocumentUrl));

Assert.AreEqual(type, exception.ServerErrorTypeName);
observer.Verify(log => log.Error(It.Is<LogEntry>(entry =>
entry.Message.Contains(DocumentUrl) && entry.Exception == exception)), Times.Once);
observer.Verify(log => log.Warning(It.IsAny<LogEntry>()), Times.Never);
}
}

[DataTestMethod]
[DataRow("SharePointPnP.Modernization.WikiEmbedPart", "DocumentUrl")]
[DataRow("Microsoft.SharePoint.Publishing.WebControls.MediaWebPart", "MediaSource")]
[DataRow("Microsoft.Office.Excel.WebUI.ExcelWebRenderer", "WorkbookUri")]
[DataRow("Microsoft.Office.Visio.Server.WebControls.VisioWebAccess", "DiagramPath")]
public void DocumentEmbedMapping_WhenFileIsNotFound_ProcessesFunctionsAndPreservesReference(string type, string urlProperty)
{
using (var context = CreateContext())
{
var transformation = LoadMapping();
var mapping = transformation.WebParts.Single(part => part.Type.Split(',')[0] == type);
mapping.Properties = mapping.Properties.Concat(new[]
{
new Property { Name = "Host", Type = PropertyType.@string }
}).ToArray();
var webPart = new WebPartEntity { Type = mapping.Type };
foreach (var property in mapping.Properties)
{
webPart.Properties[property.Name] = string.Empty;
}
webPart.Properties[urlProperty] = DocumentUrl;
webPart.Properties["Host"] = Host;
webPart.Properties["SiteId"] = "c6815669-f27c-4ff6-b937-0825b2cdfa93";
webPart.Properties["WebId"] = "9d03e0aa-cb94-4463-b885-15a55d03ed52";

var processor = new FunctionProcessor(context, context, null, transformation,
new PageTransformationInformation(null) { SkipTelemetry = true });
SetErrorResponse(context, "System.IO.FileNotFoundException", "File Not Found.", -2147024894);

processor.Process(ref mapping, webPart);

foreach (var key in MetadataKeys)
{
Assert.AreEqual(string.Empty, webPart.Properties[key], key);
Assert.IsTrue(mapping.Properties.Any(property => property.Name == key), key);
}
Assert.AreEqual(string.Empty, webPart.Properties["EncodedAuthor"]);
var json = WebUtility.HtmlDecode(TokenParser.ReplaceTokens(
mapping.Mappings.Mapping.Single().ClientSideWebPart.Single().JsonControlData, webPart));
foreach (var property in mapping.Properties)
{
Assert.IsFalse(json.Contains("{" + property.Name + "}"), property.Name);
}
using (var document = JsonDocument.Parse(json))
{
var properties = document.RootElement.GetProperty("properties");
Assert.AreEqual(Host + DocumentUrl, properties.GetProperty("file").GetString());
Assert.AreEqual(string.Empty, properties.GetProperty("listId").GetString());
Assert.AreEqual(string.Empty, properties.GetProperty("uniqueId").GetString());
Assert.AreEqual(string.Empty, properties.GetProperty("authorName").GetString());
Assert.AreEqual(DocumentUrl, document.RootElement.GetProperty("serverProcessedContent")
.GetProperty("links").GetProperty("serverRelativeUrl").GetString());
}
}
}

private static void AssertEmptyMetadata(Dictionary<string, string> result)
{
Assert.IsNotNull(result);
CollectionAssert.AreEquivalent(MetadataKeys, result.Keys.ToArray());
foreach (var key in MetadataKeys)
{
Assert.AreEqual(string.Empty, result[key], key);
}
}

private static BuiltIn CreateBuiltIn(ClientContext context, ILogObserver observer = null)
{
return new BuiltIn(new PageTransformationInformation(null) { SkipTelemetry = true }, context,
logObservers: observer == null ? null : new[] { observer });
}

private static ClientContext CreateContext()
{
CacheManager.Instance.SetSharePointVersion(new Uri(SiteUrl), SPVersion.SPO);
var provider = new MockEntryResponseProvider();
foreach (var property in new[] { "Site", "Web" })
{
provider.ResponseEntries.Add(new MockResponseEntry<object>
{
Url = SiteUrl,
PropertyName = property,
ReturnValue = new Dictionary<string, object>
{
["_ObjectType_"] = "SP." + property,
["ServerRelativeUrl"] = WebServerRelativeUrl,
["Url"] = SiteUrl
}
});
}
provider.ResponseEntries.Add(new MockResponseEntry<object>
{
Url = SiteUrl,
Method = "GetFileByServerRelativeUrl",
ReturnValue = new Dictionary<string, object>
{
["_ObjectType_"] = "SP.File",
["ListId"] = $"/Guid({DocumentListId})/",
["UniqueId"] = $"/Guid({DocumentUniqueId})/",
["Author"] = new Dictionary<string, object>
{
["_ObjectType_"] = "SP.User",
["LoginName"] = "i:0#.f|membership|author@contoso.com",
["Title"] = "Document Author"
}
}
});
return new ClientContext(SiteUrl)
{
WebRequestExecutorFactory = new MockWebRequestExecutorFactory(provider)
};
}

private static void SetErrorResponse(ClientContext context, string type, string message, int code)
{
context.Load(context.Web, web => web.ServerRelativeUrl);
context.ExecuteQuery();
var provider = new Mock<IMockResponseProvider>();
provider.Setup(response => response.GetResponse(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(JsonSerializer.Serialize(new[]
{
new
{
SchemaVersion = "15.0.0.0",
LibraryVersion = "16.0.0.0",
ErrorInfo = new { ErrorMessage = message, ErrorValue = (string)null, ErrorCode = code, ErrorTypeName = type },
TraceCorrelationId = "d991e178-2b53-4ff8-b77f-6bb73516962a"
}
}));
context.WebRequestExecutorFactory = new MockWebRequestExecutorFactory(provider.Object);
}

private static PageTransformation LoadMapping()
{
var assembly = typeof(BuiltIn).Assembly;
var resourceName = assembly.GetManifestResourceNames()
.Single(name => name.EndsWith("Modernization.webpartmapping.xml", StringComparison.Ordinal));
using (var stream = assembly.GetManifestResourceStream(resourceName))
{
return (PageTransformation)new XmlSerializer(typeof(PageTransformation)).Deserialize(stream);
}
}
}
}
46 changes: 24 additions & 22 deletions src/lib/PnP.Framework/Modernization/Functions/BuiltIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -845,23 +845,26 @@ public string ExtractWebpartProperties(string clientSideWebPartHtml)
#endregion

#region DocumentEmbed functions
[FunctionDocumentation(Description = "Does lookup a file based on the given server relative path and return needed properties of the file. Returns null if file was not found.",
[FunctionDocumentation(Description = "Looks up file properties by server relative path. Returns empty metadata when the path is empty or the file cannot be found, allowing mappings to retain the document URL. A missing file is logged as a warning; the document itself is not recovered.",
Example = "DocumentEmbedLookup({ServerRelativeFileName})")]
[InputDocumentation(Name = "{ServerRelativeFileName}", Description = "Server relative file name")]
[OutputDocumentation(Name = "{DocumentWeb}", Description = "Server relative URL of the web holding the file")]
[OutputDocumentation(Name = "{DocumentListId}", Description = "Id of the list holding the file")]
[OutputDocumentation(Name = "{DocumentUniqueId}", Description = "UniqueId of the file")]
[OutputDocumentation(Name = "{DocumentAuthor}", Description = "User principal name of the document author")]
[OutputDocumentation(Name = "{DocumentAuthorName}", Description = "Name of the file author")]
public Dictionary<string, string> DocumentEmbedLookup(string serverRelativeUrl)
{
Dictionary<string, string> results = new Dictionary<string, string>();
Dictionary<string, string> results = new Dictionary<string, string>
{
["DocumentWeb"] = "",
["DocumentListId"] = "",
["DocumentUniqueId"] = "",
["DocumentAuthor"] = "",
["DocumentAuthorName"] = ""
};
if (string.IsNullOrEmpty(serverRelativeUrl))
{
results.Add("DocumentWeb", "");
results.Add("DocumentListId", "");
results.Add("DocumentUniqueId", "");
results.Add("DocumentAuthor", "");
results.Add("DocumentAuthorName", "");
return results;
}

Expand All @@ -882,15 +885,15 @@ public Dictionary<string, string> DocumentEmbedLookup(string serverRelativeUrl)

contextToUse = this.clientContext.Clone(webUrlResult.Value);
}
catch (ServerException ex) when (ex.ServerErrorTypeName == "System.IO.FileNotFoundException")
{
LogWarning(string.Format(LogStrings.Warning_DocumentEmbedLookupFileNotFound, serverRelativeUrl, this.clientContext.Url), LogStrings.Heading_BuiltInFunctions);
return results;
}
catch (Exception ex)
{
LogError(LogStrings.Error_DocumentEmbedLookup, LogStrings.Heading_BuiltInFunctions, ex);
LogError($"{LogStrings.Error_DocumentEmbedLookup}: {serverRelativeUrl}", LogStrings.Heading_BuiltInFunctions, ex);

results.Add("DocumentWeb", "");
results.Add("DocumentListId", "");
results.Add("DocumentUniqueId", "");
results.Add("DocumentAuthor", "");
results.Add("DocumentAuthorName", "");
return results;
}
}
Expand All @@ -904,25 +907,24 @@ public Dictionary<string, string> DocumentEmbedLookup(string serverRelativeUrl)

string[] authorParts = document.Author.LoginName.Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries);

results.Add("DocumentWeb", contextToUse.Web.ServerRelativeUrl);
results.Add("DocumentListId", document.ListId.ToString());
results.Add("DocumentUniqueId", document.UniqueId.ToString());
results.Add("DocumentAuthor", authorParts.Length == 3 ? authorParts[2] : "");
results.Add("DocumentAuthorName", document.Author.Title);
results["DocumentWeb"] = contextToUse.Web.ServerRelativeUrl;
results["DocumentListId"] = document.ListId.ToString();
results["DocumentUniqueId"] = document.UniqueId.ToString();
results["DocumentAuthor"] = authorParts.Length == 3 ? authorParts[2] : "";
results["DocumentAuthorName"] = document.Author.Title;

return results;
}
catch (ServerException ex)
{
if (ex.ServerErrorTypeName == "System.IO.FileNotFoundException")
{
// provided file is not retrievable...we're eating the exception this file not be used in the target web part
LogError(LogStrings.Error_DocumentEmbedLookupFileNotRetrievable, LogStrings.Heading_BuiltInFunctions, ex);
return null;
LogWarning(string.Format(LogStrings.Warning_DocumentEmbedLookupFileNotFound, serverRelativeUrl, contextToUse.Url), LogStrings.Heading_BuiltInFunctions);
return results;
}
else
{
LogError(LogStrings.Error_DocumentEmbedLookup, LogStrings.Heading_BuiltInFunctions, ex);
LogError($"{LogStrings.Error_DocumentEmbedLookup}: {serverRelativeUrl}", LogStrings.Heading_BuiltInFunctions, ex);
throw;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ public static class LogStrings
public const string Error_ReturnCrossSiteRelativePath = "An error occurred in ReturnCrossSiteRelativePath function";
public const string Error_DocumentEmbedLookup = "An error occurred in DocumentEmbedLookup function";
public const string Error_DocumentEmbedLookupFileNotRetrievable = "An error occurred in DocumentEmbedLookup function - file not retrievable";
public const string Warning_DocumentEmbedLookupFileNotFound = "DocumentEmbedLookup received File Not Found for '{0}' (context: '{1}'). Retaining the document reference with empty metadata so page transformation can continue. Verify the URL, document existence, and the migration identity's access; this response does not establish why the file is unavailable.";
public const string Error_LoadContentFromFile = "An error occurred in LoadContentFromFile function";

public const string Error_LoadContentFromFileContentLink = "An error occurred in getting the referenced file in content link";
Expand Down
Loading