diff --git a/MS-WOPIFoldersTestCases.xml b/MS-WOPIFoldersTestCases.xml new file mode 100644 index 0000000..a6980f9 --- /dev/null +++ b/MS-WOPIFoldersTestCases.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + This tests that hosts' CheckFolderInfo responses conform to the JSON schema. + + + + + + + + + + + + + + + + + + + This tests that hosts' EnumerateChildren responses conform to the JSON schema. + + + + + + + + + + + + + + + + + + + This tests that Version in EnumerateChildren response matches the value in Version field in CheckFileInfo response. + + + + + + + + + + + + + + + + + + + + + + + + + + + This tests that Version in EnumerateChildren response changes when the file changes. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MS-WOPITestCases.xml b/MS-WOPITestCases.xml new file mode 100644 index 0000000..4f38760 --- /dev/null +++ b/MS-WOPITestCases.xml @@ -0,0 +1,1948 @@ + + + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the user has permission to perform Write operation. + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the user is denied permission to call PutRelativeFile. + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if file can be changed by the user. + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the user has permission to call PutRelativeFile. + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the host declares support for Lock/Unlock/RefreshLock/UnlockAndRefreshLock + operations. + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if host declares support for PutFile/PutRelativeFile operations. + + + + + + + + + + + + + + + The prereq BusinessFlowPrereq must pass prior to running the feature validations related to business flows. + + + + + + + + + + + + + + + The host uses FileUrl for direct file access. + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the host sets SupportedShareUrlTypes in CheckFileInfo to any value. + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if host declares support for RenameFile operation. + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the host declares support for GetRestrictedLink/RevokeRestrictedLink + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the host declares support for ReadSecureStore + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the host declares support for EnumerateChildren and DeleteFile + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the host declares support for the "ReadOnly" Share Url type for the file. + + + + + + + + + + + + + + + Prereq WOPI Validation test to check if the host declares support for the "ReadWrite" Share Url type for the file. + + + + + + + + + + + + + + + + + + This tests that hosts' CheckFileInfo responses conform to the JSON schema. + + + + + + + + + + + + + + + This tests that host returns 401 or 404 response for a CheckFileInfo request with invalid access token. + + + + + + + + + + + + + + + + + + + + + FileEditingPrereq + UserCanWritePrereq + + + + + This tests that file version is changed when the file content changes. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This tests that get a file successfully. + + + + + + + + + + + + + + + + + + + + This tests that host returns 401 or 404 response for a GetFile request with invalid access token. + + + + + + + + + + + + + + + + + + + + + FileEditingPrereq + UserCanWriteRelativePrereq + LocksPrereq + SupportsFoldersPrereq + + + + + Tests the basic PutRelativeFile scenario where a suggested extension is specified. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests the basic PutRelativeFile scenario where a suggested name is specified. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests the PutRelativeFile scenario where a suggested name is specified but + a file with the target name already exists. Expects the request to succeed with the host + choosing a suitable name. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests the PutRelativeFile scenario where a Relative name is specified. Expects the created file to have + the exact name specified. + + + + + + + + + + + + + + + + + + + + + + + + + Tests the PutRelativeFile scenario where a Relative name is specified and OverwriteRelative + is set to true. Since no file with target name exists in this scenario, the header should have + no effect. + + + + + + + + + + + + + + + + + + + + + + + + + Tests the PutRelativeFile scenario where a Relative name is specified and OverwriteRelative + is set to false. Since no file with target name exists in this scenario, the header should have + no effect. + + + + + + + + + + + + + + + + + + + + + + + + + Tests the PutRelativeFile scenario where a Relative name is specified and OverwriteRelative + is not specified. Since a file with target name exists in this scenario, this should return a 409. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests the PutRelativeFile scenario where a Relative name is specified and OverwriteRelative + is set to false. Since a file with target name exists in this scenario, this should return a 409. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests the PutRelativeFile scenario where a Relative name is specified and OverwriteRelative is set to true. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests the PutRelativeFile scenario where a relative name is specified along with OverwriteRelative + set to true, but a file with the same target name already exists and is locked. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests that file name is encoded correctly after a PutRelativeFile operation. + + + + + + + + + + + + + + + + + + + + + + + + FileEditingPrereq + NotReadOnlyPrereq + UserCanWritePrereq + LocksPrereq + + + + + This tests that put a file successfully. + + + + + + + + + + + + + + + + + + + This tests that put a file with matching lock value successfully after lock. + + + + + + + + + + + + + + + + + + + This tests that put a file with matching lock value successfully after unlock and relock. + + + + + + + + + + + + + + + + + + + + This tests that host returns 401 or 404 response for a PutFile request with invalid access token. + + + + + + + + + + + + + + + + + + + + + + + + + FileEditingPrereq + LocksPrereq + UserCanWritePrereq + + + + + This tests that lock mismatch when PutFile with incorrect lock id. + + + + + + + + + + + + + + + + + + + + LocksPrereq + + + + + Simulates a successful sequence of lock-related requests: Lock, RefreshLock, UnlockAndRelock, Unlock. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This tests that lock mismatch when trying to refresh the lock with incorrect lock id. + + + + + + + + + + + + + + + + + + This tests that lock mismatch when trying to unlock with incorrect lock id. + + + + + + + + + + + + + + + + + + + This tests that lock mismatch when trying to refresh lock with incorrect lock id. + + + + + + + + + + + + + + + + + + This tests that lock mismatch when trying to unlock and relock with incorrect lock id. + + + + + + + + + + + + + + + + + + + This tests that lock mismatch when unlock with old lock. + + + + + + + + + + + + + + + + + + + This tests that host returns 401 or 404 response for locks requests with invalid access token. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LocksPrereq + + + + + This tests that X-WOPI-Lock header is returned when lock mismatch for Lock request. + + + + + + + + + + + + + + + + + + + This tests that X-WOPI-Lock header is included when responding with the 409 status code for Unlock request if no lock exists on the file. + + + + + + + + + + + + + This tests that X-WOPI-Lock header is included when responding with the 409 status code for RefreshLock request if no lock exists on the file. + + + + + + + + + + + + + + + + This tests that X-WOPI-Lock header is included when responding with the 409 status code for UnlockAndRelock request if no lock exists on the file. + + + + + + + + + + + + + + + + + + LocksPrereq + + + + + This tests that X-WOPI-Lock header is not returned when lock success. + + + + + + + + + + + + + + + + This tests that X-WOPI-Lock header is not returned when Unlock success. + + + + + + + + + + + + + + + + + This tests that X-WOPI-Lock header is not returned when RefreshLock success. + + + + + + + + + + + + + + + + + This tests that X-WOPI-Lock header is not returned when UnlockAndRelock succeess. + + + + + + + + + + + + + + + + + + + + UserCanWriteRelativePrereq + FileEditingPrereq + SupportsFoldersPrereq + + + + + Tests CheckFileInfo operation returns 404 status code if file is unknown. + + + + + + + + + + + + + + + + + + + + + + + + Tests GetFile operation returns 404 status code if file is unknown. + + + + + + + + + + + + + + + + + + + + + + + + Tests DeleteFile operation returns 404 status code if file is unknown. + + + + + + + + + + + + + + + + + + + + + + + + Tests PutRelativeFile operation returns 404 status code if file is unknown. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UserCanWriteRelativePrereq + LocksPrereq + FileEditingPrereq + SupportsFoldersPrereq + + + + + Tests Lock operation returns 404 status code if file is unknown. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests Unlock operation returns 404 status code if file is unknown. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests RefreshLock operation returns 404 status code if file is unknown. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests UnlockAndRelock operation returns 404 status code if file is unknown. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UserCanWriteRelativePrereq + LocksPrereq + FileEditingPrereq + SupportsFoldersPrereq + + + + + Tests that host returns status code 404 when update unknown file. + + + + + + + + + + + + + + + + + + + + + + + + + + FileEditingPrereq + LocksPrereq + SupportsFoldersPrereq + UserCanWriteRelativePrereq + + + + + Tests that rename a file successfully. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests that file name is encoded correctly after rename file. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests that host returns 404 status code for unknown file. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests that host returns 401 or 403 status code for RenameFile request with invalid access token. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tests that rename file should succeed or failed with status code 400 if rename to an existing file name. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SupportsScenarioLinksPrereq + + + + + Tests the GetRestrictedLink operation for a file + + + + + + + + + + + + + + + + Simulates a GetRestrictedLink request with invalid access token and expects a 404 response. + + + + + + + + + + + + + + + + + + + + + FileEditingPrereq + SupportsFoldersPrereq + SupportsScenarioLinksPrereq + UserCanWriteRelativePrereq + + + + + Tests that GetRestrictedLink operation should fail with a 404 status code for a deleted file. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SupportsScenarioLinksPrereq + + + + + Tests the RevokeRestrictedLink operation for a file + + + + + + + + + + + + + + + + Tests that a RevokeRestrictedLink request with invalid access token expects a 404 response. + + + + + + + + + + + + + + + + + + + + + FileEditingPrereq + SupportsFoldersPrereq + SupportsScenarioLinksPrereq + UserCanWriteRelativePrereq + + + + + Tests that RevokeRestrictedLink operation should fail with a 404 status code for a deleted file. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SupportsSecureStorePrereq + + + + + Tests the ReadSecureStore operation for a file + + + + + + + + + + + + + + + + + + + + Tests that ReadSecureStore request with invalid access token expects a 401 or 404 response. + + + + + + + + + + + + + + + + + + + + + UserCanWriteRelativePrereq + FileEditingPrereq + SupportsFoldersPrereq + SupportsSecureStorePrereq + + + + + Tests ReadSecureStore operation returns 404 status code if file is unknown. + + + + + + + + + + + + + + + + + + + + + + + + + + SupportsSecureStorePrereq + + + + + Tests host returns a value for X-WOPI-PerfTrace if the header X-WOPI-PerfTraceRequested in the request is present and equal to "true". + + + + + + + + + + + + diff --git a/TestCases.xsd b/TestCases.xsd index 70db83a..72c21ff 100644 --- a/TestCases.xsd +++ b/TestCases.xsd @@ -49,6 +49,7 @@ + @@ -144,6 +145,8 @@ + + @@ -281,6 +284,7 @@ + @@ -292,6 +296,7 @@ + @@ -305,6 +310,7 @@ + @@ -377,6 +383,7 @@ + @@ -420,6 +427,7 @@ + @@ -453,6 +461,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/WopiValidator.Core/ConfigParser.cs b/src/WopiValidator.Core/ConfigParser.cs index 187a80c..2b0d29f 100644 --- a/src/WopiValidator.Core/ConfigParser.cs +++ b/src/WopiValidator.Core/ConfigParser.cs @@ -10,9 +10,9 @@ namespace Microsoft.Office.WopiValidator.Core { public static class ConfigParser { - public static IEnumerable ParseExecutionData(string filePath, TestCategory targetTestCategory, string testGroupName = "") + public static IEnumerable ParseExecutionData(string filePath, string applicationId = null, string usingRestrictedScenario = null) { - return ParseExecutionData(filePath, new ResourceManagerFactory(), new TestCaseFactory(), testGroupName, targetTestCategory); + return ParseExecutionData(filePath, new ResourceManagerFactory(), new TestCaseFactory(), applicationId, usingRestrictedScenario); } /// @@ -22,8 +22,8 @@ public static IEnumerable ParseExecutionData( string filePath, IResourceManagerFactory resourceManagerFactory, ITestCaseFactory testCaseFactory, - string testGroupName, - TestCategory targetTestCategory) + string applicationId, + string usingRestrictedScenario) { XDocument xDoc = XDocument.Load(filePath); @@ -31,11 +31,11 @@ public static IEnumerable ParseExecutionData( IResourceManager resourceManager = resourceManagerFactory.GetResourceManager(resourcesElement); XElement prereqCasesElement = xDoc.Root.Element("PrereqCases") ?? new XElement("PrereqCases"); - IEnumerable prereqCases = testCaseFactory.GetTestCases(prereqCasesElement, targetTestCategory); + IEnumerable prereqCases = testCaseFactory.GetTestCases(prereqCasesElement, applicationId, usingRestrictedScenario); Dictionary prereqCasesDictionary = prereqCases.ToDictionary(e => e.Name); return xDoc.Root.Elements("TestGroup") - .SelectMany(x => GetTestExecutionDataForGroup(x, prereqCasesDictionary, testCaseFactory, resourceManager, targetTestCategory)); + .SelectMany(x => GetTestExecutionDataForGroup(x, prereqCasesDictionary, testCaseFactory, resourceManager, applicationId, usingRestrictedScenario)); } private static IEnumerable GetTestExecutionDataForGroup( @@ -43,11 +43,12 @@ private static IEnumerable GetTestExecutionDataForGroup( Dictionary prereqCasesDictionary, ITestCaseFactory testCaseFactory, IResourceManager resourceManager, - TestCategory targetTestCategory) + string applicationId, + string usingRestrictedScenario) { IEnumerable prereqs; IEnumerable groupTestCases; - testCaseFactory.GetTestCases(definition, prereqCasesDictionary, out prereqs, out groupTestCases, targetTestCategory); + testCaseFactory.GetTestCases(definition, prereqCasesDictionary, out prereqs, out groupTestCases, applicationId, usingRestrictedScenario); List prereqList = prereqs.ToList(); diff --git a/src/WopiValidator.Core/Constants.cs b/src/WopiValidator.Core/Constants.cs index e8038ac..6e083f4 100644 --- a/src/WopiValidator.Core/Constants.cs +++ b/src/WopiValidator.Core/Constants.cs @@ -35,6 +35,10 @@ public static class Headers public const string OverwriteRelative = "X-WOPI-OverwriteRelativeTarget"; public const string Version = "X-WOPI-ItemVersion"; public const string UrlType = "X-WOPI-UrlType"; + public const string RestrictedLink = "X-WOPI-RestrictedLink"; + public const string UsingRestrictedScenario = "X-WOPI-UsingRestrictedScenario"; + public const string ApplicationId = "X-WOPI-ApplicationId"; + public const string PerfTraceRequested = "X-WOPI-PerfTraceRequested"; // This is not an official WOPI header; it is used to pass exception information // back to the validator UI. See the ExceptionHelper class for more details. @@ -65,6 +69,9 @@ public static class Overrides public const string GetShareUrl = "GET_SHARE_URL"; public const string AddActivities = "ADD_ACTIVITIES"; public const string PutUserInfo = "PUT_USER_INFO"; + public const string GetRestrictedLink = "GET_RESTRICTED_LINK"; + public const string RevokeRestrictedLink = "REVOKE_RESTRICTED_LINK"; + public const string ReadSecureStore = "READ_SECURE_STORE"; } public static class RequestMethods @@ -101,6 +108,10 @@ public static class Requests public const string GetShareUrl = "GetShareUrl"; public const string AddActivities = "AddActivities"; public const string PutUserInfo = "PutUserInfo"; + public const string GetRestrictedLink = "GetRestrictedLink"; + public const string RevokeRestrictedLink = "RevokeRestrictedLink"; + public const string ReadSecureStore = "ReadSecureStore"; + public const string CheckFolderInfo = "CheckFolderInfo"; } public static class Validators diff --git a/src/WopiValidator.Core/DiscoveryListener.cs b/src/WopiValidator.Core/DiscoveryListener.cs new file mode 100644 index 0000000..7762c1b --- /dev/null +++ b/src/WopiValidator.Core/DiscoveryListener.cs @@ -0,0 +1,278 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Xml; +using System.Xml.Serialization; + +namespace Microsoft.Office.WopiValidator.Core +{ + public class DiscoveryListener + { + private TcpListener listener = null; + public string proofKey; + public string proofKeyOld; + private string progid = null; + private int port = -1; + + public DiscoveryListener(string proofKey, string proofKeyOld, int port = 80, string progid = "OneNote.Notebook") + { + this.proofKey = proofKey; + this.proofKeyOld = proofKeyOld; + this.progid = progid; + this.port = port; + } + + public void Start() + { + if (listener == null) + { + IPAddress address = IPAddress.Any; + IPEndPoint endPoint = new IPEndPoint(address, this.port); + listener = new TcpListener(endPoint); + } + + try + { + listener.Start(); + listener.BeginAcceptTcpClient(HandleRequest, null); + } + catch (Exception ex) + { + + } + } + + private void HandleRequest(IAsyncResult result) + { + TcpClient client = listener.EndAcceptTcpClient(result); + NetworkStream netstream = client.GetStream(); + + listener.BeginAcceptTcpClient(HandleRequest, null); + + byte[] buffer = new byte[2048]; + + int receivelength = netstream.Read(buffer, 0, 2048); + string requeststring = Encoding.UTF8.GetString(buffer, 0, receivelength); + + if (!requeststring.StartsWith(@"GET /hosting/discovery", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + string xmlBody = GetDiscoveryResponseXmlString(); + + string statusLine = "HTTP/1.1 200 OK\r\n"; + byte[] responseStatusLineBytes = Encoding.UTF8.GetBytes(statusLine); + + string responseHeader = + string.Format( + "Content-Type: text/xml; charset=UTf-8\r\nContent-Length: {0}\r\n", xmlBody.Length); + byte[] responseHeaderBytes = Encoding.UTF8.GetBytes(responseHeader); + + byte[] responseBodyBytes = Encoding.UTF8.GetBytes(xmlBody); + + netstream.Write(responseStatusLineBytes, 0, responseStatusLineBytes.Length); + netstream.Write(responseHeaderBytes, 0, responseHeaderBytes.Length); + netstream.Write(new byte[] { 13, 10 }, 0, 2); + netstream.Write(responseBodyBytes, 0, responseBodyBytes.Length); + client.Close(); + } + + /// + /// Discovery WOPI discovery response xml. It indicates the WOPI client supports 4 types file extensions: ".txt", ".zip", ".one" , ".wopitest" + /// + /// Discovery response xml. + public string GetDiscoveryResponseXmlString() + { + ct_wopidiscovery wopiDiscovery = new ct_wopidiscovery(); + + // Add http and https net zone into the wopiDiscovery + wopiDiscovery.netzone = GetNetZones(); + + // ProofKey element + wopiDiscovery.proofkey = new ct_proofkey(); + wopiDiscovery.proofkey.oldvalue = proofKeyOld; + wopiDiscovery.proofkey.value = proofKey; + string xmlStringOfResponseDiscovery = GetDiscoveryXmlFromDiscoveryObject(wopiDiscovery); + + return xmlStringOfResponseDiscovery; + } + + /// + /// Get internal http and internal https ct_netzone. + /// + /// An array of ct_netzone type instances. + private ct_netzone[] GetNetZones() + { + string fakedWOPIClientActionHostName = string.Format(@"{0}.com", Guid.NewGuid().ToString("N")); + + // HTTP net zone + ct_netzone httpNetZone = GetSingleNetZoneForWopiDiscoveryResponse(st_wopizone.internalhttp, fakedWOPIClientActionHostName); + + // HTTPS Net zone + ct_netzone httpsNetZone = GetSingleNetZoneForWopiDiscoveryResponse(st_wopizone.internalhttps, fakedWOPIClientActionHostName); + + return new ct_netzone[] { httpNetZone, httpsNetZone }; + } + + /// + /// Get a single ct_netzone type instance for current test client. + /// + /// protocol and intended network-type + /// Host name for the action of the WOPI client supports. + /// A ct_netzone type instance. + private ct_netzone GetSingleNetZoneForWopiDiscoveryResponse(st_wopizone netZoneType, string fakedWOPIClientActionHostName) + { + string clientName = Dns.GetHostName(); + + string transportValue = st_wopizone.internalhttp == netZoneType ? Uri.UriSchemeHttp : Uri.UriSchemeHttps; + Random radomInstance = new Random((int)DateTime.UtcNow.Ticks & 0x0000FFFF); + string appName = string.Format( + @"MSWOPITESTAPP {0} _for {1} WOPIServer_{2}", + radomInstance.Next(), + clientName, + netZoneType); + + Uri favIconUrlValue = new Uri( + string.Format(@"{0}://{1}/wv/resources/1033/FavIcon_Word.ico", transportValue, fakedWOPIClientActionHostName), + UriKind.Absolute); + + Uri urlsrcValueOfTextFile = new Uri( + string.Format(@"{0}://{1}/wv/wordviewerframe.aspx?<ui=UI_LLCC&><rs=DC_LLCC&><showpagestats=PERFSTATS&>", transportValue, fakedWOPIClientActionHostName), + UriKind.Absolute); + + Uri urlsrcValueOfZipFile = new Uri( + string.Format(@"{0}://{1}/wv/zipviewerframe.aspx?<ui=UI_LLCC&><rs=DC_LLCC&><showpagestats=PERFSTATS&>", transportValue, fakedWOPIClientActionHostName), + UriKind.Absolute); + + Uri urlsrcValueOfUsingprogid = new Uri( + string.Format(@"{0}://{1}/o/onenoteframe.aspx?edit=0&<ui=UI_LLCC&><rs=DC_LLCC&><showpagestats=PERFSTATS&>", transportValue, fakedWOPIClientActionHostName), + UriKind.Absolute); + + // Setting netZone's sub element's values + ct_appname appElement = new ct_appname(); + appElement.name = appName; + appElement.favIconUrl = favIconUrlValue.OriginalString; + appElement.checkLicense = true; + + // Action element for txt file + ct_wopiaction actionForTextFile = new ct_wopiaction(); + actionForTextFile.name = st_wopiactionvalues.view; + actionForTextFile.ext = "txt"; + actionForTextFile.requires = "containers"; + actionForTextFile.@default = true; + actionForTextFile.urlsrc = urlsrcValueOfTextFile.OriginalString; + + // Action element for txt file + ct_wopiaction formeditactionForTextFile = new ct_wopiaction(); + formeditactionForTextFile.name = st_wopiactionvalues.formedit; + formeditactionForTextFile.ext = "txt"; + formeditactionForTextFile.@default = true; + formeditactionForTextFile.urlsrc = urlsrcValueOfTextFile.OriginalString; + + ct_wopiaction formViewactionForTextFile = new ct_wopiaction(); + formViewactionForTextFile.name = st_wopiactionvalues.formsubmit; + formViewactionForTextFile.ext = "txt"; + formViewactionForTextFile.@default = true; + formViewactionForTextFile.urlsrc = urlsrcValueOfTextFile.OriginalString; + + // Action element for zip file + ct_wopiaction actionForZipFile = new ct_wopiaction(); + actionForZipFile.name = st_wopiactionvalues.view; + actionForZipFile.ext = "zip"; + actionForZipFile.@default = true; + actionForZipFile.urlsrc = urlsrcValueOfZipFile.OriginalString; + + // Action elements for one note. + ct_wopiaction actionForOneNote = new ct_wopiaction(); + actionForOneNote.name = st_wopiactionvalues.view; + actionForOneNote.ext = "one"; + actionForOneNote.requires = "cobalt"; + actionForOneNote.@default = true; + actionForOneNote.urlsrc = urlsrcValueOfUsingprogid.OriginalString; + + // Action elements for one note. + ct_wopiaction actionForOneNoteProg = new ct_wopiaction(); + actionForOneNoteProg.name = st_wopiactionvalues.view; + actionForOneNoteProg.progid = progid; + actionForOneNoteProg.requires = "cobalt,containers"; + actionForOneNoteProg.@default = true; + actionForOneNoteProg.urlsrc = urlsrcValueOfUsingprogid.OriginalString; + + // Action element for wopitest file + ct_wopiaction actionForWopitestFile = new ct_wopiaction(); + actionForWopitestFile.name = st_wopiactionvalues.view; + actionForWopitestFile.ext = "wopitest"; + actionForWopitestFile.requires = "containers"; + actionForWopitestFile.@default = true; + actionForWopitestFile.urlsrc = urlsrcValueOfTextFile.OriginalString; + + ct_wopiaction formeditactionForWopitestFile = new ct_wopiaction(); + formeditactionForWopitestFile.name = st_wopiactionvalues.formedit; + formeditactionForWopitestFile.ext = "wopitest"; + formeditactionForWopitestFile.@default = true; + formeditactionForWopitestFile.urlsrc = urlsrcValueOfTextFile.OriginalString; + + ct_wopiaction formViewactionForWopitestFile = new ct_wopiaction(); + formViewactionForWopitestFile.name = st_wopiactionvalues.formsubmit; + formViewactionForWopitestFile.ext = "wopitest"; + formViewactionForWopitestFile.@default = true; + formViewactionForWopitestFile.urlsrc = urlsrcValueOfTextFile.OriginalString; + + // Add action elements into the app element. + appElement.action = new ct_wopiaction[] { + actionForTextFile, + actionForOneNote, + actionForZipFile, + formeditactionForTextFile, + formViewactionForTextFile, + actionForOneNoteProg, + actionForWopitestFile, + formeditactionForWopitestFile, + formViewactionForWopitestFile }; + + // Add app element into the netzone element. + ct_netzone netZoneInstance = new ct_netzone(); + netZoneInstance.app = new ct_appname[] { appElement }; + netZoneInstance.name = netZoneType; + netZoneInstance.nameSpecified = true; + return netZoneInstance; + } + + /// + /// Get a xml string from a WOPI Discovery type object. + /// + /// ct_wopidiscovery instance. + /// xml string which contains discovery information. + public string GetDiscoveryXmlFromDiscoveryObject(ct_wopidiscovery wopiDiscovery) + { + XmlSerializer xmlSerializer = new XmlSerializer(typeof(ct_wopidiscovery)); + string xmlString = string.Empty; + + MemoryStream memorySteam = new MemoryStream(); + StreamWriter streamWriter = new StreamWriter(memorySteam, Encoding.UTF8); + + // Remove w3c default namespace prefix in serialize process. + XmlSerializerNamespaces nameSpaceInstance = new XmlSerializerNamespaces(); + nameSpaceInstance.Add(string.Empty, string.Empty); + xmlSerializer.Serialize(streamWriter, wopiDiscovery, nameSpaceInstance); + + // Read the MemoryStream to output the xml string. + memorySteam.Position = 0; + using (StreamReader streamReader = new StreamReader(memorySteam)) + { + xmlString = streamReader.ReadToEnd(); + } + + streamWriter.Dispose(); + memorySteam.Dispose(); + + // Format the serialized xml string. + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xmlString); + return xmlDoc.OuterXml; + } + } +} diff --git a/src/WopiValidator.Core/Factories/ITestCaseFactory.cs b/src/WopiValidator.Core/Factories/ITestCaseFactory.cs index be16cf3..abdded6 100644 --- a/src/WopiValidator.Core/Factories/ITestCaseFactory.cs +++ b/src/WopiValidator.Core/Factories/ITestCaseFactory.cs @@ -12,11 +12,13 @@ public interface ITestCaseFactory /// Parse XML run configuration to get list of Test Cases /// /// ]]> element from run configuration XML file. - /// This helps to select the correct test cases. + /// application argument + /// application argument /// Collection of Test Cases. IEnumerable GetTestCases( XElement definitions, - TestCategory targetTestCategory); + string applicationId, + string usingRestrictedScenario); /// /// Parse XML run configuration testgroup element to get a list of TestCases. @@ -25,11 +27,13 @@ IEnumerable GetTestCases( /// Dictionary of name to testcase already parsed from ]]> element from run configuration file. /// PrereqCases applicable to testcases in this test group. /// TestCases in this test group. - /// This helps to select the correct test cases. + /// application argument + /// application argument void GetTestCases(XElement definition, Dictionary prereqCasesDictionary, out IEnumerable prereqTests, out IEnumerable groupTests, - TestCategory targetTestCategory); + string applicationId, + string usingRestrictedScenario); } } diff --git a/src/WopiValidator.Core/Factories/RequestFactory.cs b/src/WopiValidator.Core/Factories/RequestFactory.cs index b8cf2a7..2f577d4 100644 --- a/src/WopiValidator.Core/Factories/RequestFactory.cs +++ b/src/WopiValidator.Core/Factories/RequestFactory.cs @@ -15,15 +15,15 @@ class RequestFactory /// /// Parses requests information from XML into a collection of IWopiRequest /// - public static IEnumerable GetRequests(XElement definition) + public static IEnumerable GetRequests(XElement definition, string applicationId, string usingRestrictedScenario) { - return definition.Elements().Select(GetRequest); + return definition.Elements().Select(x => GetRequest(x, applicationId, usingRestrictedScenario)); } /// /// Parses single request definition and instantiates proper IWopiRequest instance based on element name /// - private static IRequest GetRequest(XElement definition) + private static IRequest GetRequest(XElement definition, string applicationId, string usingRestrictedScenario) { string elementName = definition.Name.LocalName; XElement validatorsDefinition = definition.Element("Validators"); @@ -51,6 +51,10 @@ private static IRequest GetRequest(XElement definition) UrlType = (string)definition.Attribute("UrlType"), Validators = validators ?? GetDefaultValidators(), WopiSrc = (string)definition.Attribute("WopiSrc"), + RestrictedLinkType = (string)definition.Attribute("RestrictedLink"), + ApplicationId = applicationId, + UsingRestrictedScenario = usingRestrictedScenario, + PerfTraceRequested = string.IsNullOrEmpty((string)definition.Attribute("PerfTraceRequested")) ? false : Boolean.Parse((string)definition.Attribute("PerfTraceRequested")) }; if (requestBodyDefinition != null && !String.IsNullOrEmpty(requestBodyDefinition.Value)) @@ -128,7 +132,14 @@ private static IRequest GetRequest(XElement definition) return new AddActivitiesRequest(wopiRequestParams); case Constants.Requests.PutUserInfo: return new PutUserInfoRequest(wopiRequestParams); - + case Constants.Requests.GetRestrictedLink: + return new GetRestrictedLinkRequest(wopiRequestParams); + case Constants.Requests.RevokeRestrictedLink: + return new RevokeRestrictedLinkRequest(wopiRequestParams); + case Constants.Requests.ReadSecureStore: + return new ReadSecureStoreRequest(wopiRequestParams); + case Constants.Requests.CheckFolderInfo: + return new CheckFolderInfoRequest(wopiRequestParams); default: throw new ArgumentException(string.Format("Unknown request: '{0}'", elementName)); } diff --git a/src/WopiValidator.Core/Factories/TestCaseFactory.cs b/src/WopiValidator.Core/Factories/TestCaseFactory.cs index c54fa3d..8e4e495 100644 --- a/src/WopiValidator.Core/Factories/TestCaseFactory.cs +++ b/src/WopiValidator.Core/Factories/TestCaseFactory.cs @@ -12,9 +12,9 @@ namespace Microsoft.Office.WopiValidator.Core.Factories { public class TestCaseFactory : ITestCaseFactory { - public IEnumerable GetTestCases(XElement definitions, TestCategory targetTestCategory) + public IEnumerable GetTestCases(XElement definitions, string applicationId, string usingRestrictedScenario) { - return definitions.Elements("TestCase").Where(x => DoesTestCategoryMatchTargetTestCategory(x, targetTestCategory)).Select(x => GetTestCase(x)); + return definitions.Elements("TestCase").Select(x => GetTestCase(x, applicationId, usingRestrictedScenario)); } public void GetTestCases( @@ -22,13 +22,14 @@ public void GetTestCases( Dictionary prereqCasesDictionary, out IEnumerable prereqTests, out IEnumerable groupTests, - TestCategory targetTestCategory) + string applicationId, + string usingRestrictedScenario) { XElement prereqsElement = definition.Element("PrereqTests") ?? new XElement("PrereqTests"); prereqTests = GetPrereqTests(prereqsElement, prereqCasesDictionary); XElement testCasesElement = definition.Element("TestCases") ?? new XElement("TestCases"); - groupTests = GetTestCases(testCasesElement, targetTestCategory); + groupTests = GetTestCases(testCasesElement, applicationId, usingRestrictedScenario); } private static IEnumerable GetPrereqTests(XElement definition, Dictionary prereqsDictionary) @@ -49,7 +50,7 @@ private static IEnumerable GetPrereqTests(XElement definition, Dictio /// /// User RequestFactory.GetRequests to parse requests defined in that Test Case. /// - private static ITestCase GetTestCase(XElement definition) + private static ITestCase GetTestCase(XElement definition, string applicationId, string usingRestrictedScenario) { string category = (string)definition.Attribute("Category"); string name = (string)definition.Attribute("Name"); @@ -59,12 +60,12 @@ private static ITestCase GetTestCase(XElement definition) string failMessage = (string)definition.Attribute("FailMessage"); XElement requestsDefinition = definition.Element("Requests"); - IEnumerable requests = RequestFactory.GetRequests(requestsDefinition); + IEnumerable requests = RequestFactory.GetRequests(requestsDefinition, applicationId, usingRestrictedScenario); IEnumerable cleanupRequests = null; XElement cleanupRequestsDefinition = definition.Element("CleanupRequests"); if (cleanupRequestsDefinition != null) - cleanupRequests = RequestFactory.GetRequests(cleanupRequestsDefinition); + cleanupRequests = RequestFactory.GetRequests(cleanupRequestsDefinition, applicationId, usingRestrictedScenario); ITestCase testCase = new TestCase(requests, cleanupRequests, @@ -80,31 +81,6 @@ private static ITestCase GetTestCase(XElement definition) return testCase; } - /// - /// This function helps ensure that, - /// We are getting all the TestCases if the targetTestCategory is set to "All" - /// We are getting all the TestCases with "WopiCore" as their "Category", regardless of the targetTestCategory. - /// The rest of the test cases are picked up if their "Category" matches the targetTestCategory. - /// - private static bool DoesTestCategoryMatchTargetTestCategory(XElement definition, TestCategory targetTestCategory) - { - string category = (string)definition.Attribute("Category"); - string name = (string)definition.Attribute("Name"); - - if (string.IsNullOrEmpty(category)) - { - throw new Exception(string.Format(CultureInfo.InvariantCulture, "The category of {0} TestCase is empty", name)); - } - - TestCategory testCaseCategory; - if (!Enum.TryParse(category, true /* ignoreCase */, out testCaseCategory)) - { - throw new Exception(string.Format(CultureInfo.InvariantCulture, "The category of {0} TestCase is invalid", name)); - } - - return targetTestCategory == TestCategory.All || testCaseCategory == TestCategory.WopiCore || targetTestCategory == testCaseCategory; - } - /// /// Condenses a multi-line string into a more compact form. /// diff --git a/src/WopiValidator.Core/Factories/ValidatorFactory.cs b/src/WopiValidator.Core/Factories/ValidatorFactory.cs index 1f156ac..5cc62b8 100644 --- a/src/WopiValidator.Core/Factories/ValidatorFactory.cs +++ b/src/WopiValidator.Core/Factories/ValidatorFactory.cs @@ -77,8 +77,10 @@ private static IValidator GetResponseHeaderValidator(XElement definition) string expectedValue = (string)definition.Attribute("ExpectedValue"); bool isRequired = ((bool?)definition.Attribute("IsRequired")) ?? true; bool shouldMatch = ((bool?)definition.Attribute("ShouldMatch")) ?? true; + bool isUrl = ((bool?)definition.Attribute("IsUrl")) ?? false; + bool isExcluded = ((bool?)definition.Attribute("IsExcluded")) ?? false; - return new ResponseHeaderValidator(header, expectedValue, expectedStateKey, isRequired, shouldMatch); + return new ResponseHeaderValidator(header, expectedValue, expectedStateKey, isRequired, shouldMatch, isUrl, isExcluded); } /// @@ -168,15 +170,16 @@ private static JsonContentValidator.IJsonPropertyValidator GetJsonPropertyValida expectedStateKey); case Constants.Validators.Properties.StringProperty: - return new JsonContentValidator.JsonStringPropertyValidator(key, - isRequired, - expectedValue, - hasExpectedValue, - endsWithValue, - expectedStateKey, - ignoreCase); - - case Constants.Validators.Properties.StringRegexProperty: + return new JsonContentValidator.JsonStringPropertyValidator(key, + isRequired, + expectedValue, + hasExpectedValue, + endsWithValue, + expectedStateKey, + ignoreCase, + shouldMatch); + + case Constants.Validators.Properties.StringRegexProperty: return new JsonContentValidator.JsonStringRegexPropertyValidator(key, isRequired, expectedValue, diff --git a/src/WopiValidator.Core/IFilterOptions.cs b/src/WopiValidator.Core/IFilterOptions.cs new file mode 100644 index 0000000..f23fa7d --- /dev/null +++ b/src/WopiValidator.Core/IFilterOptions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Microsoft.Office.WopiValidator.Core +{ + public interface IFilterOptions + { + string TestName { get; set; } + TestCategory? TestCategory { get; set; } + string TestGroup { get; set; } + } + + public static class IFilterOptionsExtensions + { + public static IEnumerable ApplyFilters(this IEnumerable testData, IFilterOptions options) + { + var toReturn = testData; + + // Filter by test name + if (!string.IsNullOrEmpty(options.TestName)) + { + toReturn = toReturn.Where(t => t.TestCase.Name == options.TestName); + if (toReturn.Count() == 1) + { + return toReturn; + } + } + + if (options.TestCategory != null) + { + toReturn = toReturn.Where(t => t.TestCategoryMatches(options.TestCategory) == true); + } + + if (!string.IsNullOrEmpty(options.TestGroup)) + { + toReturn = toReturn.Where(t => t.TestGroupName.Equals(options.TestGroup, StringComparison.InvariantCultureIgnoreCase)); + } + + return toReturn; + } + + public static IEnumerable ApplyToData(this IFilterOptions filters, IEnumerable testData) + { + return testData.ApplyFilters(filters); + } + } +} diff --git a/src/WopiValidator.Core/ITestCase.cs b/src/WopiValidator.Core/ITestCase.cs index f878b01..93bfed2 100644 --- a/src/WopiValidator.Core/ITestCase.cs +++ b/src/WopiValidator.Core/ITestCase.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Collections.Generic; @@ -22,5 +22,7 @@ public interface ITestCase string FailMessage { get; set; } string Category { get; } + + TestCategory TestCategory { get; } } } diff --git a/src/WopiValidator.Core/JsonSchemas/MS-WOPICheckFileInfoSchema.json b/src/WopiValidator.Core/JsonSchemas/MS-WOPICheckFileInfoSchema.json new file mode 100644 index 0000000..a32797d --- /dev/null +++ b/src/WopiValidator.Core/JsonSchemas/MS-WOPICheckFileInfoSchema.json @@ -0,0 +1,650 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Full WOPI CheckFileInfo JSON Schema", + "description": "A WOPI CheckFileInfo JSON response", + "type": "object", + "additionalProperties": false, + "properties": { + "AADUserObjectId": { + "type": "string", + "default": "", + "optional": true + }, + "AccessTokenExpiry": { + "type": "integer", + "default": 0, + "optional": true + }, + "AllowAddActivitiesUserBatching": { + "type": "boolean", + "default": false, + "optional": true + }, + "AllowAdditionalMicrosoftServices": { + "type": "boolean", + "default": false, + "optional": true + }, + "AllowEarlyFeatures": { + "type": "boolean", + "default": false, + "optional": true + }, + "AllowErrorReportPrompt": { + "type": "boolean", + "default": false, + "optional": true + }, + "AllowExternalMarketplace": { + "type": "boolean", + "default": false, + "optional": true + }, + "AppCatalogUrl": { + "type": "string", + "default": "", + "optional": true + }, + "AppliedPolicyId": { + "type": "string", + "default": "", + "optional": true + }, + "BaseFileName": { + "type": "string", + "default": "", + "optional": false + }, + "BreadcrumbBrandName": { + "type": "string", + "default": "", + "optional": true + }, + "BreadcrumbBrandUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "BreadcrumbDocName": { + "type": "string", + "default": "", + "optional": true + }, + "BreadcrumbDocUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "BreadcrumbFolderName": { + "type": "string", + "default": "", + "optional": true + }, + "BreadcrumbFolderUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "ClientUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "CloseButtonClosesWindow": { + "type": "boolean", + "default": false, + "optional": true + }, + "ClosePostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "CloseUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "DirectInvokeDAVUrl": { + "type": "string", + "default": "", + "optional": true + }, + "DisableBrowserCachingOfUserContent": { + "type": "boolean", + "default": false, + "optional": true + }, + "DisablePrint": { + "type": "boolean", + "default": false, + "optional": true + }, + "DisableTranslation": { + "type": "boolean", + "default": false, + "optional": true + }, + "DownloadUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "EditAndReplyUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "EditingCannotSave": { + "type": "boolean", + "default": false, + "optional": true + }, + "EditModePostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "EditNotificationPostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "EmbeddingPageOrigin": { + "type": "string", + "default": "", + "optional": true + }, + "EmbeddingPageSessionInfo": { + "type": "string", + "default": "", + "optional": true + }, + "EnabledApplicationFeatures": { + "type": "array", + "default": [], + "optional": true + }, + "FileEmbedCommandPostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "FileEmbedCommandUrl": { + "type": "string", + "default": "", + "optional": true + }, + "FileExtension": { + "type": "string", + "default": "", + "optional": true + }, + "FileNameMaxLength": { + "type": "integer", + "default": 250, + "optional": true + }, + "FileSharingPostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "FileSharingUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "FileUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "FileVersionPostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "FileVersionUrl": { + "type": "string", + "default": "", + "optional": true + }, + "HostAuthenticationId": { + "type": "string", + "default": "", + "optional": true + }, + "HostAuthenticationIdType": { + "type": "string", + "default": "", + "optional": true + }, + "HostDivSyndicationViewUrl": { + "type": "string", + "default": "", + "optional": true + }, + "HostEditUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "HostEmbeddedEditUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "HostEmbeddedViewUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "HostName": { + "type": "string", + "default": "", + "optional": true + }, + "HostNotes": { + "type": "string", + "default": "", + "optional": true + }, + "HostRestUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "HostViewUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "InsertImagePostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "IrmPolicyDescription": { + "type": "string", + "default": "", + "optional": true + }, + "IrmPolicyTitle": { + "type": "string", + "default": "", + "optional": true + }, + "IsAnonymousUser": { + "type": "boolean", + "default": false, + "optional": true + }, + "IsEduUser": { + "type": "boolean", + "default": false, + "optional": true + }, + "IsYammerEnabled": { + "type": "boolean", + "default": false, + "optional": true + }, + "LastModifiedTime": { + "type": "string", + "default": "", + "optional": true + }, + "LicenseCheckForEditIsEnabled": { + "type": "boolean", + "default": false, + "optional": true + }, + "LicensedOrganization": { + "type": "string", + "default": "", + "optional": true + }, + "OfficeCollaborationServiceEndpointUrl": { + "type": "string", + "default": "", + "optional": true + }, + "OpenInClientCommandUrl": { + "type": "string", + "default": "", + "optional": true + }, + "OpenInClientPostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "OwnerId": { + "type": "string", + "optional": false + }, + "PermissionsPostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "PolicyCheckPostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "PostMessageOrigin": { + "type": "string", + "default": "", + "optional": true + }, + "PresenceProvider": { + "type": "string", + "default": "", + "optional": true + }, + "PresenceUserId": { + "type": "string", + "default": "", + "optional": true + }, + "PrivacyUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "ProtectedFile": { + "type": "boolean", + "default": false, + "optional": true + }, + "ProtectInClient": { + "type": "boolean", + "default": false, + "optional": true + }, + "ReadOnly": { + "type": "boolean", + "default": false, + "optional": true + }, + "ReportAbusePostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "ReportAbuseUrl": { + "type": "string", + "default": "", + "optional": true + }, + "RestrictedWebViewOnly": { + "type": "boolean", + "default": false, + "optional": true + }, + "SafeLinksStatus": { + "type": "string", + "default": "", + "optional": true + }, + "SHA256": { + "type": "string", + "default": "", + "optional": true + }, + "SignInUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "SignoutUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "Size": { + "type": "integer", + "default": -1, + "optional": false + }, + "SupportedShareUrlTypes": { + "type": "array", + "default": [], + "optional": true + }, + "SupportsAddActivities": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsCheckPolicy": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsCheckUserAccess": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsCoauth": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsCobalt": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsContactsResolution": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsContainers": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsDeleteFile": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsEcosystem": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsExtendedLockLength": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsFileCreation": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsFileUserValue": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsFolders": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsGetActivities": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsGetLock": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsGrantUserAccess": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsLocks": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsPolicies": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsRename": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsReviewing": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsScenarioLinks": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsSecureStore": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsUpdate": { + "type": "boolean", + "default": false, + "optional": true + }, + "SupportsUserInfo": { + "type": "boolean", + "default": false, + "optional": true + }, + "TenantId": { + "type": "string", + "default": "", + "optional": true + }, + "TermsOfUseUrl": { + "type": "string", + "format": "uri", + "default": "", + "optional": true + }, + "TimeZone": { + "type": "string", + "default": "", + "optional": true + }, + "UniqueContentId": { + "type": "string", + "default": "", + "optional": true + }, + "UserCanAttend": { + "type": "boolean", + "default": false, + "optional": true + }, + "UserCanNotWriteRelative": { + "type": "boolean", + "default": false, + "optional": true + }, + "UserCanPresent": { + "type": "boolean", + "default": false, + "optional": true + }, + "UserCanRename": { + "type": "boolean", + "default": false, + "optional": true + }, + "UserCanReview": { + "type": "boolean", + "default": false, + "optional": true + }, + "UserCanWrite": { + "type": "boolean", + "default": false, + "optional": true + }, + "UserFriendlyName": { + "type": "string", + "default": "", + "optional": true + }, + "UserId": { + "type": "string", + "default": "", + "optional": true + }, + "UserInfo": { + "type": "string", + "default": "", + "optional": true + }, + "UserPrincipalName": { + "type": "string", + "default": "", + "optional": true + }, + "Version": { + "type": "string", + "optional": false + }, + "WebEditingDisabled": { + "type": "boolean", + "default": false, + "optional": true + }, + "WorkflowPostMessage": { + "type": "boolean", + "default": false, + "optional": true + }, + "WorkflowType": { + "type": "array", + "default": [], + "optional": true + }, + "WorkflowUrl": { + "type": "string", + "default": "", + "optional": true + } + } +} diff --git a/src/WopiValidator.Core/JsonSchemas/MS-WOPICheckFolderInfoSchema.json b/src/WopiValidator.Core/JsonSchemas/MS-WOPICheckFolderInfoSchema.json new file mode 100644 index 0000000..649071b --- /dev/null +++ b/src/WopiValidator.Core/JsonSchemas/MS-WOPICheckFolderInfoSchema.json @@ -0,0 +1,265 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Full WOPI CheckFolderInfo JSON Schema", + "description": "A WOPI CheckFolderInfo JSON response", + "type": "object", + "additionalProperties": false, + "required": [ + "FolderName", + "OwnerId" + ], + "properties": { + "AADUserObjectId": { + "type": "string", + "default": "" + }, + "AccessTokenExpiry": { + "type": "integer", + "default": 0 + }, + "AllowEarlyFeatures": { + "type": "boolean", + "default": false + }, + "AllowExternalMarketplace": { + "type": "boolean", + "default": false + }, + "AppCatalogUrl": { + "type": "string", + "default": "" + }, + "FolderName": { + "type": "string", + "default": "" + }, + "BreadcrumbBrandIconUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "BreadcrumbBrandName": { + "type": "string", + "default": "" + }, + "BreadcrumbBrandUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "BreadcrumbDocName": { + "type": "string", + "default": "" + }, + "BreadcrumbDocUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "BreadcrumbFolderName": { + "type": "string", + "default": "" + }, + "BreadcrumbFolderUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "ClientUrl": { + "type": "string", + "default": "" + }, + "CloseButtonClosesWindow": { + "type": "boolean", + "default": false + }, + "ClosePostMessage": { + "type": "boolean", + "default": false + }, + "CloseUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "DirectInvokeDAVUrl": { + "type": "string", + "default": "" + }, + "DisablePrint": { + "type": "boolean", + "default": false + }, + "EditNotificationPostMessage": { + "type": "boolean", + "default": false + }, + "FileSharingPostMessage": { + "type": "boolean", + "default": false + }, + "FileSharingUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "FileVersionPostMessage": { + "type": "boolean", + "default": false + }, + "FileVersionUrl": { + "type": "string", + "default": "" + }, + "HostAuthenticationId": { + "type": "string", + "default": "" + }, + "HostAuthenticationIdType": { + "type": "string", + "default": "" + }, + "HostEditUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "HostEmbeddedEditUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "HostEmbeddedViewUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "HostName": { + "type": "string", + "default": "" + }, + "HostNotes": { + "type": "string", + "default": "" + }, + "HostViewUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "IrmPolicyDescription": { + "type": "string", + "default": "" + }, + "IrmPolicyTitle": { + "type": "string", + "default": "" + }, + "IsAnonymousUser": { + "type": "boolean", + "default": false + }, + "OpenInClientCommandUrl": { + "type": "string", + "default": "" + }, + "OpenInClientPostMessage": { + "type": "boolean", + "default": false + }, + "OwnerId": { + "type": "string" + }, + "PostMessageOrigin": { + "type": "string", + "default": "" + }, + "PresenceProvider": { + "type": "string", + "default": "" + }, + "PresenceUserId": { + "type": "string", + "default": "" + }, + "PrivacyUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "ProtectInClient": { + "type": "boolean", + "default": false + }, + "ReportAbusePostMessage": { + "type": "boolean", + "default": false + }, + "ReportAbuseUrl": { + "type": "string", + "default": "" + }, + "SafeLinksStatus": { + "type": "string", + "default": "" + }, + "SignoutUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "SupportsFileUserValue": { + "type": "boolean", + "default": false + }, + "SupportsSecureStore": { + "type": "boolean", + "default": false + }, + "TenantId": { + "type": "string", + "default": "" + }, + "TermsOfUseUrl": { + "type": "string", + "format": "uri", + "default": "" + }, + "UserCanReview": { + "type": "boolean", + "default": false + }, + "UserCanWrite": { + "type": "boolean", + "default": false + }, + "UserFriendlyName": { + "type": "string", + "default": "" + }, + "UserId": { + "type": "string", + "default": "" + }, + "UserPrincipalName": { + "type": "string", + "default": "" + }, + "WebEditingDisabled": { + "type": "boolean", + "default": false + }, + "WorkflowPostMessage": { + "type": "boolean", + "default": false + }, + "WorkflowType": { + "type": "array", + "default": [] + }, + "WorkflowUrl": { + "type": "string", + "default": "" + } + } +} diff --git a/src/WopiValidator.Core/JsonSchemas/ReadSecureStoreSchema.json b/src/WopiValidator.Core/JsonSchemas/ReadSecureStoreSchema.json new file mode 100644 index 0000000..8d3179c --- /dev/null +++ b/src/WopiValidator.Core/JsonSchemas/ReadSecureStoreSchema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Full WOPI ReadSecureStore JSON Schema", + "description": "A WOPI ReadSecureStore JSON response", + "type": "object", + "additionalProperties": false, + "required": [ + "UserName", + "Password" + ], + "properties": { + "UserName": { + "type": "string" + }, + "Password": { + "type": "string" + }, + "IsWindowsCredentials": { + "type": "boolean" + }, + "IsGroup": { + "type": "boolean" + } + } +} diff --git a/src/WopiValidator.Core/Requests/CheckFolderInfoRequest.cs b/src/WopiValidator.Core/Requests/CheckFolderInfoRequest.cs new file mode 100644 index 0000000..6f001ad --- /dev/null +++ b/src/WopiValidator.Core/Requests/CheckFolderInfoRequest.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Office.WopiValidator.Core.Requests +{ + class CheckFolderInfoRequest : WopiRequest + { + public CheckFolderInfoRequest(WopiRequestParam param) : base(param) + { + } + + public override string Name { get { return Constants.Requests.CheckFolderInfo; } } + protected override string RequestMethod { get { return Constants.RequestMethods.Get; } } + protected override string WopiOverrideValue { get { return null; } } + } +} diff --git a/src/WopiValidator.Core/Requests/GetRestrictedLinkRequest.cs b/src/WopiValidator.Core/Requests/GetRestrictedLinkRequest.cs new file mode 100644 index 0000000..3d42fda --- /dev/null +++ b/src/WopiValidator.Core/Requests/GetRestrictedLinkRequest.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Office.WopiValidator.Core.Requests +{ + class GetRestrictedLinkRequest : WopiRequest + { + public GetRestrictedLinkRequest(WopiRequestParam param) : base(param) + { + this.RestrictedLinkType = param.RestrictedLinkType; + this.UsingRestrictedScenario = param.UsingRestrictedScenario; + } + + public string RestrictedLinkType { get; private set; } + public string UsingRestrictedScenario { get; private set; } + public override string Name { get { return Constants.Requests.GetRestrictedLink; } } + protected override string WopiOverrideValue { get { return Constants.Overrides.GetRestrictedLink; } } + protected override IEnumerable> DefaultHeaders + { + get + { + Dictionary headers = new Dictionary(); + headers.Add(Constants.Headers.RestrictedLink, RestrictedLinkType); + + if (!string.IsNullOrEmpty(UsingRestrictedScenario)) + { + headers.Add(Constants.Headers.UsingRestrictedScenario, UsingRestrictedScenario); + } + + return headers; + } + } + } +} diff --git a/src/WopiValidator.Core/Requests/ReadSecureStoreRequest.cs b/src/WopiValidator.Core/Requests/ReadSecureStoreRequest.cs new file mode 100644 index 0000000..8dfef4f --- /dev/null +++ b/src/WopiValidator.Core/Requests/ReadSecureStoreRequest.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Office.WopiValidator.Core.Requests +{ + class ReadSecureStoreRequest : WopiRequest + { + public ReadSecureStoreRequest(WopiRequestParam param) : base(param) + { + this.ApplicationId = param.ApplicationId; + this.PerfTraceRequested = param.PerfTraceRequested; + } + + public string ApplicationId { get; private set; } + public bool PerfTraceRequested { get; private set; } + public override string Name { get { return Constants.Requests.ReadSecureStore; } } + protected override string WopiOverrideValue { get { return Constants.Overrides.ReadSecureStore; } } + + protected override IEnumerable> GetCustomHeaders(Dictionary savedState, IResourceManager resourceManager) + { + if (string.IsNullOrEmpty(this.ApplicationId)) + { + throw new System.Exception("No value provided for header 'X-WOPI-ApplicationId' in ReadSecureStore request! \n Provide value for header 'X-WOPI-ApplicationId' by using command line argument '--ApplicationId'."); + } + + Dictionary headers = new Dictionary(); + headers.Add(Constants.Headers.ApplicationId, this.ApplicationId); + + if (this.PerfTraceRequested) + { + headers.Add(Constants.Headers.PerfTraceRequested, System.Boolean.TrueString); + } + + return headers; + } + } +} diff --git a/src/WopiValidator.Core/Requests/RevokeRestrictedLinkRequest.cs b/src/WopiValidator.Core/Requests/RevokeRestrictedLinkRequest.cs new file mode 100644 index 0000000..b4e1efd --- /dev/null +++ b/src/WopiValidator.Core/Requests/RevokeRestrictedLinkRequest.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Office.WopiValidator.Core.Requests +{ + class RevokeRestrictedLinkRequest : WopiRequest + { + public RevokeRestrictedLinkRequest(WopiRequestParam param) : base(param) + { + this.RestrictedLinkType = param.RestrictedLinkType; + this.UsingRestrictedScenario = param.UsingRestrictedScenario; + } + + public string RestrictedLinkType { get; private set; } + public string UsingRestrictedScenario { get; private set; } + public override string Name { get { return Constants.Requests.RevokeRestrictedLink; } } + protected override string WopiOverrideValue { get { return Constants.Overrides.RevokeRestrictedLink; } } + protected override IEnumerable> DefaultHeaders + { + get + { + Dictionary headers = new Dictionary(); + headers.Add(Constants.Headers.RestrictedLink, RestrictedLinkType); + + if (!string.IsNullOrEmpty(UsingRestrictedScenario)) + { + headers.Add(Constants.Headers.UsingRestrictedScenario, UsingRestrictedScenario); + } + + return headers; + } + } + } +} diff --git a/src/WopiValidator.Core/Requests/WopiRequestParam.cs b/src/WopiValidator.Core/Requests/WopiRequestParam.cs index 04cd590..5feb647 100644 --- a/src/WopiValidator.Core/Requests/WopiRequestParam.cs +++ b/src/WopiValidator.Core/Requests/WopiRequestParam.cs @@ -26,6 +26,10 @@ public struct WopiRequestParam public IEnumerable Validators { get; set; } public string WopiSrc { get; set; } public string UrlType { get; set; } + public string RestrictedLinkType { get; set; } + public string UsingRestrictedScenario { get; set; } + public string ApplicationId { get; set; } + public bool PerfTraceRequested { get; set; } } public enum PutRelativeFileMode diff --git a/src/WopiValidator.Core/TestCase.cs b/src/WopiValidator.Core/TestCase.cs index 09d20b3..e020bc3 100644 --- a/src/WopiValidator.Core/TestCase.cs +++ b/src/WopiValidator.Core/TestCase.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -7,46 +7,57 @@ namespace Microsoft.Office.WopiValidator.Core { - /// - /// Represents a single test case. - /// - class TestCase : ITestCase - { - public TestCase( - IEnumerable requests, - IEnumerable cleanupRequests, - string name, - string description, - string category) - { - if (requests == null) - throw new ArgumentNullException("requests"); - Requests = requests.ToArray(); - if (!Requests.Any()) - throw new ArgumentException("TestCase has to have at least one request.", "requests"); + /// + /// Represents a single test case. + /// + internal class TestCase : ITestCase + { + public TestCase( + IEnumerable requests, + IEnumerable cleanupRequests, + string name, + string description, + string category) + { + if (requests == null) + throw new ArgumentNullException("requests"); + Requests = requests.ToArray(); + if (!Requests.Any()) + throw new ArgumentException("TestCase has to have at least one request.", "requests"); - if (cleanupRequests == null) - cleanupRequests = Enumerable.Empty(); - CleanupRequests = cleanupRequests.ToArray(); + if (cleanupRequests == null) + cleanupRequests = Enumerable.Empty(); + CleanupRequests = cleanupRequests.ToArray(); - if (string.IsNullOrEmpty(name)) - throw new ArgumentException("Name cannot be empty.", "name"); - Name = name; + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Name cannot be empty.", "name"); + Name = name; - Description = description; - UiScreenShot = String.Empty; - DocumentationLink = String.Empty; - FailMessage = String.Empty; - Category = category; - } + Description = description; + UiScreenShot = String.Empty; + DocumentationLink = String.Empty; + FailMessage = String.Empty; + Category = category; + } - public IEnumerable Requests { get; private set; } - public IEnumerable CleanupRequests { get; private set; } - public string Name { get; private set; } - public string Description { get; private set; } - public string UiScreenShot { get; set; } - public string DocumentationLink { get; set; } - public string FailMessage {get; set; } - public string Category { get; private set; } - } + public IEnumerable Requests { get; private set; } + public IEnumerable CleanupRequests { get; private set; } + public string Name { get; private set; } + public string Description { get; private set; } + public string UiScreenShot { get; set; } + public string DocumentationLink { get; set; } + public string FailMessage { get; set; } + public string Category { get; private set; } + public TestCategory TestCategory + { + get + { + if (!Enum.TryParse(Category, true /* ignoreCase */, out TestCategory testCategory)) + { + throw new Exception($"Invalid TestCategory: {Category}"); + } + return testCategory; + } + } + } } diff --git a/src/WopiValidator.Core/TestCaseExecutor.cs b/src/WopiValidator.Core/TestCaseExecutor.cs index 1602721..343aafa 100644 --- a/src/WopiValidator.Core/TestCaseExecutor.cs +++ b/src/WopiValidator.Core/TestCaseExecutor.cs @@ -119,6 +119,12 @@ private TestCaseResult ExecuteTestCase(ITestCase testCase) requestDetails.Add(requestInfo); + // Save any state that was requested + foreach (IStateEntry stateSaver in request.State) + { + savedState[stateSaver.Name] = stateSaver.GetValue(responseData); + } + // return on the first request that fails if (validationFailures.Any()) { @@ -135,11 +141,7 @@ private TestCaseResult ExecuteTestCase(ITestCase testCase) break; } - // Save any state that was requested - foreach (IStateEntry stateSaver in request.State) - { - savedState[stateSaver.Name] = stateSaver.GetValue(responseData); - } + } } finally diff --git a/src/WopiValidator.Core/TestCategory.cs b/src/WopiValidator.Core/TestCategory.cs new file mode 100644 index 0000000..9bc0009 --- /dev/null +++ b/src/WopiValidator.Core/TestCategory.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Xml.Linq; + +namespace Microsoft.Office.WopiValidator.Core +{ + public enum TestCategory + { + All = 0, + WopiCore = 1, + OfficeOnline = 2, + OfficeNativeClient = 3 + } + + internal static class TestCategoryExtensions + { + /// + /// This function applies the rules of category filtering. If this returns true, the test should be included based on the category. + /// + /// The rules to apply are as follows: + /// If the filterCategory is All or null, the test should be included. + /// If the test's category is WopiCore, it should be included (all WopiCore tests should always be included). + /// If the test's category matches the filterCategory, it should be included. + /// + internal static bool TestCategoryMatches(this TestExecutionData testData, TestCategory? filterCategory) + { + return TestCategoryMatches(testData.TestCase, filterCategory); + } + + /// + /// This function applies the rules of category filtering. If this returns true, the test should be included based on the category. + /// + /// The rules to apply are as follows: + /// If the filterCategory is All or null, the test should be included. + /// If the test's category is WopiCore, it should be included (all WopiCore tests should always be included). + /// If the test's category matches the filterCategory, it should be included. + /// + internal static bool TestCategoryMatches(this ITestCase testCase, TestCategory? category) + { + if (!category.HasValue || + category == TestCategory.All || + testCase.TestCategory == TestCategory.WopiCore) + { + return true; + } + + return testCase.TestCategory == category; + } + } +} diff --git a/src/WopiValidator.Core/TestExecutionData.cs b/src/WopiValidator.Core/TestExecutionData.cs index 9ee5b93..d45614e 100644 --- a/src/WopiValidator.Core/TestExecutionData.cs +++ b/src/WopiValidator.Core/TestExecutionData.cs @@ -7,14 +7,6 @@ namespace Microsoft.Office.WopiValidator.Core { - public enum TestCategory - { - All = 0, - WopiCore = 1, - OfficeOnline = 2, - OfficeNativeClient = 3 - } - public class TestExecutionData { internal TestExecutionData(ITestCase testCase, IEnumerable prereqCases, IResourceManager resourceManager, string testGroupName) diff --git a/src/WopiValidator.Core/Validators/JsonContentValidator.cs b/src/WopiValidator.Core/Validators/JsonContentValidator.cs index 325a1d3..42569a6 100644 --- a/src/WopiValidator.Core/Validators/JsonContentValidator.cs +++ b/src/WopiValidator.Core/Validators/JsonContentValidator.cs @@ -11,440 +11,447 @@ namespace Microsoft.Office.WopiValidator.Core.Validators { - /// - /// Validates that response content is a JSON encoded string that contains provided set of properties with values matching expecting ones. - /// - internal class JsonContentValidator : IValidator - { - private readonly IJsonPropertyValidator[] _propertyValidators; - - public JsonContentValidator(IJsonPropertyValidator propertyValidator = null) - { - _propertyValidators = propertyValidator == null ? new IJsonPropertyValidator[0] : new[] { propertyValidator }; - } - - public JsonContentValidator(IEnumerable propertyValidators) - { - _propertyValidators = (propertyValidators ?? Enumerable.Empty()).ToArray(); - } - - public string Name - { - get { return "JsonContentValidator"; } - } - - public ValidationResult Validate(IResponseData data, IResourceManager resourceManager, Dictionary savedState) - { - string responseContentString = data.GetResponseContentAsString(); - if (!data.IsTextResponse || String.IsNullOrEmpty(responseContentString)) - return new ValidationResult("Couldn't read resource content."); - - return ValidateJsonContent(responseContentString, savedState); - } - - private ValidationResult ValidateJsonContent(string jsonString, Dictionary savedState) - { - try - { - JObject jObject = jsonString.ParseJObject(); - - List errors = new List(); - foreach (IJsonPropertyValidator propertyValidator in _propertyValidators) - { - JToken propertyValue = jObject.SelectToken(propertyValidator.Key); - - string errorMessage; - bool result = propertyValidator.Validate(propertyValue, savedState, out errorMessage); - - if (!result) - errors.Add(string.Format("Incorrect value for '{0}' property. {1}", propertyValidator.Key, errorMessage)); - } - - if (errors.Count == 0) - return new ValidationResult(); - - return new ValidationResult(errors.ToArray()); - } - catch (JsonReaderException ex) - { - return new ValidationResult($"{Name}: {ex.GetType().Name} thrown while parsing JSON. Are you sure the response is JSON?"); - } - catch (JsonException ex) - { - return new ValidationResult($"{Name}: {ex.GetType().Name} thrown while parsing JSON content: '{ex.Message}'"); - } - } - - public interface IJsonPropertyValidator - { - string Key { get; } - bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage); - } - - public abstract class JsonPropertyValidator : IJsonPropertyValidator - { - protected JsonPropertyValidator(string key, bool isRequired) - { - Key = key; - IsRequired = isRequired; - } - - protected bool IsActualValueNullOrEmpty(JToken actualValue) - { - return (actualValue == null) || - (actualValue.Type == JTokenType.Array && !actualValue.HasValues) || - (actualValue.Type == JTokenType.Object && !actualValue.HasValues) || - (actualValue.Type == JTokenType.String && string.IsNullOrEmpty(actualValue.Value())) || - (actualValue.Type == JTokenType.Null); - } - - public string Key { get; private set; } - - public bool IsRequired { get; private set; } - - public abstract bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage); - } - - - public class JsonAbsoluteUrlPropertyValidator : JsonPropertyValidator - { - public string ExpectedStateKey { get; private set; } - private readonly bool _mustIncludeAccessToken = false; - - public JsonAbsoluteUrlPropertyValidator(string key, bool isRequired, bool mustIncludeAccessToken, string expectedStateKey) - : base(key, isRequired) - { - ExpectedStateKey = expectedStateKey; - _mustIncludeAccessToken = mustIncludeAccessToken; - } - - public override bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage) - { - errorMessage = null; - - if (IsActualValueNullOrEmpty(actualValue)) - { - if (IsRequired) - { - errorMessage = string.Format("Value is required but not provided."); - return false; - } - - return true; - } - else - { - string value = actualValue.Value(); - - Uri uri; - if (Uri.TryCreate(value, UriKind.Absolute, out uri)) - { - if (_mustIncludeAccessToken && IncludesAccessToken(value)) - { - errorMessage = $"URL '{value}' does not include the 'access_token' query parameter"; - return false; - } - - return true; - } - else - { - errorMessage = string.Format("Cannot parse {0} as absolute URL", value); - return false; - } - } - } - - /// - /// Returns true if the URI includes an access_token query string parameter; false otherwise. - /// - private bool IncludesAccessToken(string url) - { - return UrlHelper.GetQueryParameterValue(url, "access_token") == null; - } - } - - public abstract class JsonPropertyEqualityValidator : JsonPropertyValidator - where T : IEquatable - { - protected JsonPropertyEqualityValidator(string key, bool isRequired, T expectedValue, bool hasExpectedValue, string expectedStateKey) - : base(key, isRequired) - { - DefaultExpectedValue = expectedValue; - HasExpectedValue = hasExpectedValue; - ExpectedStateKey = expectedStateKey; - } - - public override bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage) - { - if (IsActualValueNullOrEmpty(actualValue)) - { - if (IsRequired) - { - errorMessage = string.Format(CultureInfo.CurrentCulture, "Required property missing"); - return false; - } - else - { - errorMessage = ""; - return true; - } - } - - // If the "ExpectedValue" and "ExpectedStateKey" attributes are non-empty on a Validator, then ExpectedStateKey will take precedence. - // But if the mentioned "ExpectedStateKey" is invalid or doesn't have a saved state value, then the logic below will default to the value set in - // "ExpectedValue" attribute of the Validator. - T expectedValue = DefaultExpectedValue; - bool hasExpectedStateValue = false; - if (savedState != null && ExpectedStateKey != null && savedState.ContainsKey(ExpectedStateKey) && !string.IsNullOrEmpty(savedState[ExpectedStateKey])) - { - try - { - expectedValue = (T)Convert.ChangeType(savedState[ExpectedStateKey], typeof(T)); - hasExpectedStateValue = true; - } - catch (FormatException) - { - if (!HasExpectedValue) - { - errorMessage = string.Format(CultureInfo.CurrentCulture, "ExpectedStateValue should be of type : {0}", typeof(T).FullName); - return false; - } - } - } - - if (!HasExpectedValue && !hasExpectedStateValue) - { - errorMessage = ""; - return true; - } - - return Compare(actualValue, expectedValue, out errorMessage); - } - - protected virtual bool Compare(JToken actualValue, T expectedValue, out string errorMessage) - { - string formattedActualValue; - bool isValid = false; - try - { - T typedActualValue = actualValue.Value(); - formattedActualValue = FormatValue(typedActualValue); - - isValid = typedActualValue.Equals(expectedValue); - } - catch (FormatException) - { - formattedActualValue = actualValue.Value(); - isValid = false; - } - - errorMessage = string.Format(CultureInfo.CurrentCulture, "Expected: '{0}', Actual: '{1}'", FormattedExpectedValue, formattedActualValue); - return isValid; - } - - public T DefaultExpectedValue { get; private set; } - - public bool HasExpectedValue { get; private set; } - - public string ExpectedStateKey { get; private set; } - - public string FormattedExpectedValue { get { return FormatValue(DefaultExpectedValue); } } - - public abstract string FormatValue(T value); - } - - public class JsonIntegerPropertyValidator : JsonPropertyEqualityValidator - { - public JsonIntegerPropertyValidator(string key, bool isRequired, int expectedValue, bool hasExpectedValue, string expectedStateKey) - : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) - { - } - - public override string FormatValue(int value) - { - return value.ToString(CultureInfo.InvariantCulture); - } - } - - public class JsonLongPropertyValidator : JsonPropertyEqualityValidator - { - public JsonLongPropertyValidator(string key, bool isRequired, long expectedValue, bool hasExpectedValue, string expectedStateKey) - : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) - { - } - - public override string FormatValue(long value) - { - return value.ToString(CultureInfo.InvariantCulture); - } - } - - public class JsonBooleanPropertyValidator : JsonPropertyEqualityValidator - { - public JsonBooleanPropertyValidator(string key, bool isRequired, bool expectedValue, bool hasExpectedValue, string expectedStateKey) - : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) - { - } - - public override string FormatValue(bool value) - { - return value.ToString(CultureInfo.InvariantCulture); - } - } - - public class JsonStringPropertyValidator : JsonPropertyEqualityValidator - { - private readonly string _endsWithValue; - private readonly bool _ignoreCase; - private readonly StringComparison _comparisonType; - - public JsonStringPropertyValidator(string key, bool isRequired, string expectedValue, bool hasExpectedValue, string endsWithValue, string expectedStateKey, bool ignoreCase = false) - : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) - { - _endsWithValue = endsWithValue; - _ignoreCase = ignoreCase; - _comparisonType = ignoreCase ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; - } - - public override string FormatValue(string value) - { - return value; - } - - public override bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage) - { - if (!base.Validate(actualValue, savedState, out errorMessage)) - return false; - - errorMessage = ""; - if (String.IsNullOrWhiteSpace(_endsWithValue)) - return true; - - string typedActualValue = actualValue.Value(); - string formattedActualValue = FormatValue(typedActualValue); - - if (!formattedActualValue.EndsWith(_endsWithValue, _comparisonType)) - { - errorMessage = string.Format("Expected to end with: '{0}', Actual: '{1}'", _endsWithValue, formattedActualValue); - return false; - } - - return true; - } - - protected override bool Compare(JToken actualValue, string expectedValue, out string errorMessage) - { - if (!_ignoreCase) - { - return base.Compare(actualValue, expectedValue, out errorMessage); - } - - string formattedActualValue = FormatValue(actualValue.Value()); - bool isValid = formattedActualValue.Equals(expectedValue, _comparisonType); - errorMessage = string.Format(CultureInfo.CurrentCulture, "Expected: '{0} (case-insensitive)', Actual: '{1}'", expectedValue, formattedActualValue); - return isValid; - } - } - - public class JsonStringRegexPropertyValidator : JsonPropertyEqualityValidator - { - private readonly Regex _regex; - private readonly bool _shouldMatch; - - public JsonStringRegexPropertyValidator(string key, bool isRequired, string expectedValue, bool hasExpectedValue, string expectedStateKey, bool shouldMatch, bool ignoreCase = false) - : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) - { - RegexOptions options = RegexOptions.Compiled; - - if (ignoreCase) - { - options = options | RegexOptions.IgnoreCase; - } - _regex = new Regex(expectedValue, options | RegexOptions.Compiled); - _shouldMatch = shouldMatch; - } - - public override string FormatValue(string value) - { - return value; - } - - public override bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage) - { - errorMessage = ""; - - if (actualValue == null && !IsRequired) - return true; - - errorMessage = ""; - string typedActualValue = actualValue.Value(); - - if (string.IsNullOrEmpty(typedActualValue)) - { - errorMessage = $"Value null doesn't match the expected regular expression '{_regex}'"; - return false; - } - - string formattedActualValue = FormatValue(typedActualValue); - - bool isMatch = _regex.IsMatch(typedActualValue); - - if (_shouldMatch) - { - if (isMatch) - { - return true; - } - errorMessage = string.Format("Value '{0}' doesn't match the expected regular expression '{1}'", formattedActualValue, _regex); - return false; - } - else // _isMatchShouldBe == false - { - if (!isMatch) - { - return true; - } - - errorMessage = string.Format("Value '{0}' matched the regular expression, but should not '{1}'", formattedActualValue, _regex); - return false; - } - } - } - - public class JsonArrayPropertyValidator : JsonPropertyEqualityValidator - { - public JsonArrayPropertyValidator(string key, bool isRequired, string containsValue, bool hasContainsValue, string expectedStateKey) - : base(key, isRequired, containsValue, hasContainsValue, expectedStateKey) - { - } - - protected override bool Compare(JToken actualArrayOfValues, string expectedValue, out string errorMessage) - { - string formattedActualValue; - bool isValid = false; - - try - { - IList typedActualValue = actualArrayOfValues.ToObject>(); - formattedActualValue = typedActualValue.ToString(); - - isValid = typedActualValue.Contains(expectedValue, StringComparer.OrdinalIgnoreCase); - } - catch (FormatException) - { - formattedActualValue = ""; - isValid = false; - } - - errorMessage = string.Format(CultureInfo.CurrentCulture, "Expected: '{0}', Actual: '{1}'", FormattedExpectedValue, formattedActualValue); - return isValid; - } - - public override string FormatValue(string value) - { - return value; - } - } - } + /// + /// Validates that response content is a JSON encoded string that contains provided set of properties with values matching expecting ones. + /// + internal class JsonContentValidator : IValidator + { + private readonly IJsonPropertyValidator[] _propertyValidators; + + public JsonContentValidator(IJsonPropertyValidator propertyValidator = null) + { + _propertyValidators = propertyValidator == null ? new IJsonPropertyValidator[0] : new[] { propertyValidator }; + } + + public JsonContentValidator(IEnumerable propertyValidators) + { + _propertyValidators = (propertyValidators ?? Enumerable.Empty()).ToArray(); + } + + public string Name + { + get { return "JsonContentValidator"; } + } + + public ValidationResult Validate(IResponseData data, IResourceManager resourceManager, Dictionary savedState) + { + string responseContentString = data.GetResponseContentAsString(); + if (!data.IsTextResponse || String.IsNullOrEmpty(responseContentString)) + return new ValidationResult("Couldn't read resource content."); + + return ValidateJsonContent(responseContentString, savedState); + } + + private ValidationResult ValidateJsonContent(string jsonString, Dictionary savedState) + { + try + { + JObject jObject = jsonString.ParseJObject(); + + List errors = new List(); + foreach (IJsonPropertyValidator propertyValidator in _propertyValidators) + { + JToken propertyValue = jObject.SelectToken(propertyValidator.Key); + + string errorMessage; + bool result = propertyValidator.Validate(propertyValue, savedState, out errorMessage); + + if (!result) + errors.Add(string.Format("Incorrect value for '{0}' property. {1}", propertyValidator.Key, errorMessage)); + } + + if (errors.Count == 0) + return new ValidationResult(); + + return new ValidationResult(errors.ToArray()); + } + catch (JsonReaderException ex) + { + return new ValidationResult($"{Name}: {ex.GetType().Name} thrown while parsing JSON. Are you sure the response is JSON?"); + } + catch (JsonException ex) + { + return new ValidationResult($"{Name}: {ex.GetType().Name} thrown while parsing JSON content: '{ex.Message}'"); + } + } + + public interface IJsonPropertyValidator + { + string Key { get; } + bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage); + } + + public abstract class JsonPropertyValidator : IJsonPropertyValidator + { + protected JsonPropertyValidator(string key, bool isRequired) + { + Key = key; + IsRequired = isRequired; + } + + protected bool IsActualValueNullOrEmpty(JToken actualValue) + { + return (actualValue == null) || + (actualValue.Type == JTokenType.Array && !actualValue.HasValues) || + (actualValue.Type == JTokenType.Object && !actualValue.HasValues) || + (actualValue.Type == JTokenType.String && string.IsNullOrEmpty(actualValue.Value())) || + (actualValue.Type == JTokenType.Null); + } + + public string Key { get; private set; } + + public bool IsRequired { get; private set; } + + public abstract bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage); + } + + + public class JsonAbsoluteUrlPropertyValidator : JsonPropertyValidator + { + public string ExpectedStateKey { get; private set; } + private readonly bool _mustIncludeAccessToken = false; + + public JsonAbsoluteUrlPropertyValidator(string key, bool isRequired, bool mustIncludeAccessToken, string expectedStateKey) + : base(key, isRequired) + { + ExpectedStateKey = expectedStateKey; + _mustIncludeAccessToken = mustIncludeAccessToken; + } + + public override bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage) + { + errorMessage = null; + + if (IsActualValueNullOrEmpty(actualValue)) + { + if (IsRequired) + { + errorMessage = string.Format("Value is required but not provided."); + return false; + } + + return true; + } + else + { + string value = actualValue.Value(); + + Uri uri; + if (Uri.TryCreate(value, UriKind.Absolute, out uri)) + { + if (_mustIncludeAccessToken && IncludesAccessToken(value)) + { + errorMessage = $"URL '{value}' does not include the 'access_token' query parameter"; + return false; + } + + return true; + } + else + { + errorMessage = string.Format("Cannot parse {0} as absolute URL", value); + return false; + } + } + } + + /// + /// Returns true if the URI includes an access_token query string parameter; false otherwise. + /// + private bool IncludesAccessToken(string url) + { + return UrlHelper.GetQueryParameterValue(url, "access_token") == null; + } + } + + public abstract class JsonPropertyEqualityValidator : JsonPropertyValidator + where T : IEquatable + { + protected JsonPropertyEqualityValidator(string key, bool isRequired, T expectedValue, bool hasExpectedValue, string expectedStateKey) + : base(key, isRequired) + { + DefaultExpectedValue = expectedValue; + HasExpectedValue = hasExpectedValue; + ExpectedStateKey = expectedStateKey; + } + + public override bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage) + { + if (IsActualValueNullOrEmpty(actualValue)) + { + if (IsRequired) + { + errorMessage = string.Format(CultureInfo.CurrentCulture, "Required property missing"); + return false; + } + else + { + errorMessage = ""; + return true; + } + } + + // If the "ExpectedValue" and "ExpectedStateKey" attributes are non-empty on a Validator, then ExpectedStateKey will take precedence. + // But if the mentioned "ExpectedStateKey" is invalid or doesn't have a saved state value, then the logic below will default to the value set in + // "ExpectedValue" attribute of the Validator. + T expectedValue = DefaultExpectedValue; + bool hasExpectedStateValue = false; + if (savedState != null && ExpectedStateKey != null && savedState.ContainsKey(ExpectedStateKey) && !string.IsNullOrEmpty(savedState[ExpectedStateKey])) + { + try + { + expectedValue = (T)Convert.ChangeType(savedState[ExpectedStateKey], typeof(T)); + hasExpectedStateValue = true; + } + catch (FormatException) + { + if (!HasExpectedValue) + { + errorMessage = string.Format(CultureInfo.CurrentCulture, "ExpectedStateValue should be of type : {0}", typeof(T).FullName); + return false; + } + } + } + + if (!HasExpectedValue && !hasExpectedStateValue) + { + errorMessage = ""; + return true; + } + + return Compare(actualValue, expectedValue, out errorMessage); + } + + protected virtual bool Compare(JToken actualValue, T expectedValue, out string errorMessage) + { + string formattedActualValue; + bool isValid = false; + try + { + T typedActualValue = actualValue.Value(); + formattedActualValue = FormatValue(typedActualValue); + + isValid = typedActualValue.Equals(expectedValue); + } + catch (FormatException) + { + formattedActualValue = actualValue.Value(); + isValid = false; + } + + errorMessage = string.Format(CultureInfo.CurrentCulture, "Expected: '{0}', Actual: '{1}'", FormatValue(expectedValue), formattedActualValue); + return isValid; + } + + public T DefaultExpectedValue { get; private set; } + + public bool HasExpectedValue { get; private set; } + + public string ExpectedStateKey { get; private set; } + + public string FormattedExpectedValue { get { return FormatValue(DefaultExpectedValue); } } + + public abstract string FormatValue(T value); + } + + public class JsonIntegerPropertyValidator : JsonPropertyEqualityValidator + { + public JsonIntegerPropertyValidator(string key, bool isRequired, int expectedValue, bool hasExpectedValue, string expectedStateKey) + : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) + { + } + + public override string FormatValue(int value) + { + return value.ToString(CultureInfo.InvariantCulture); + } + } + + public class JsonLongPropertyValidator : JsonPropertyEqualityValidator + { + public JsonLongPropertyValidator(string key, bool isRequired, long expectedValue, bool hasExpectedValue, string expectedStateKey) + : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) + { + } + + public override string FormatValue(long value) + { + return value.ToString(CultureInfo.InvariantCulture); + } + } + + public class JsonBooleanPropertyValidator : JsonPropertyEqualityValidator + { + public JsonBooleanPropertyValidator(string key, bool isRequired, bool expectedValue, bool hasExpectedValue, string expectedStateKey) + : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) + { + } + + public override string FormatValue(bool value) + { + return value.ToString(CultureInfo.InvariantCulture); + } + } + + public class JsonStringPropertyValidator : JsonPropertyEqualityValidator + { + private readonly string _endsWithValue; + private readonly bool _ignoreCase; + private readonly StringComparison _comparisonType; + private readonly bool _shouldMatch; + + public JsonStringPropertyValidator(string key, bool isRequired, string expectedValue, bool hasExpectedValue, string endsWithValue, string expectedStateKey, bool ignoreCase = false, bool shouldMatch = true) + : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) + { + _endsWithValue = endsWithValue; + _ignoreCase = ignoreCase; + _comparisonType = ignoreCase ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; + _shouldMatch = shouldMatch; + } + + public override string FormatValue(string value) + { + return value; + } + + public override bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage) + { + if (_shouldMatch & !base.Validate(actualValue, savedState, out errorMessage)) + return false; + else if (!_shouldMatch & base.Validate(actualValue, savedState, out errorMessage)) + { + errorMessage = errorMessage.Replace("Expected", "Unexpected"); + return false; + } + + errorMessage = ""; + if (String.IsNullOrWhiteSpace(_endsWithValue)) + return true; + + string typedActualValue = actualValue.Value(); + string formattedActualValue = FormatValue(typedActualValue); + + if (!formattedActualValue.EndsWith(_endsWithValue, _comparisonType)) + { + errorMessage = string.Format("Expected to end with: '{0}', Actual: '{1}'", _endsWithValue, formattedActualValue); + return false; + } + + return true; + } + + protected override bool Compare(JToken actualValue, string expectedValue, out string errorMessage) + { + if (!_ignoreCase) + { + return base.Compare(actualValue, expectedValue, out errorMessage); + } + + string formattedActualValue = FormatValue(actualValue.Value()); + bool isValid = formattedActualValue.Equals(expectedValue, _comparisonType); + errorMessage = string.Format(CultureInfo.CurrentCulture, "Expected: '{0} (case-insensitive)', Actual: '{1}'", expectedValue, formattedActualValue); + return isValid; + } + } + + public class JsonStringRegexPropertyValidator : JsonPropertyEqualityValidator + { + private readonly Regex _regex; + private readonly bool _shouldMatch; + + public JsonStringRegexPropertyValidator(string key, bool isRequired, string expectedValue, bool hasExpectedValue, string expectedStateKey, bool shouldMatch, bool ignoreCase = false) + : base(key, isRequired, expectedValue, hasExpectedValue, expectedStateKey) + { + RegexOptions options = RegexOptions.Compiled; + + if (ignoreCase) + { + options = options | RegexOptions.IgnoreCase; + } + _regex = new Regex(expectedValue, options | RegexOptions.Compiled); + _shouldMatch = shouldMatch; + } + + public override string FormatValue(string value) + { + return value; + } + + public override bool Validate(JToken actualValue, Dictionary savedState, out string errorMessage) + { + errorMessage = ""; + + if (actualValue == null && !IsRequired) + return true; + + errorMessage = ""; + string typedActualValue = actualValue.Value(); + + if (string.IsNullOrEmpty(typedActualValue)) + { + errorMessage = $"Value null doesn't match the expected regular expression '{_regex}'"; + return false; + } + + string formattedActualValue = FormatValue(typedActualValue); + + bool isMatch = _regex.IsMatch(typedActualValue); + + if (_shouldMatch) + { + if (isMatch) + { + return true; + } + errorMessage = string.Format("Value '{0}' doesn't match the expected regular expression '{1}'", formattedActualValue, _regex); + return false; + } + else // _isMatchShouldBe == false + { + if (!isMatch) + { + return true; + } + + errorMessage = string.Format("Value '{0}' matched the regular expression, but should not '{1}'", formattedActualValue, _regex); + return false; + } + } + } + + public class JsonArrayPropertyValidator : JsonPropertyEqualityValidator + { + public JsonArrayPropertyValidator(string key, bool isRequired, string containsValue, bool hasContainsValue, string expectedStateKey) + : base(key, isRequired, containsValue, hasContainsValue, expectedStateKey) + { + } + + protected override bool Compare(JToken actualArrayOfValues, string expectedValue, out string errorMessage) + { + string formattedActualValue; + bool isValid = false; + + try + { + IList typedActualValue = actualArrayOfValues.ToObject>(); + formattedActualValue = typedActualValue.ToString(); + + isValid = typedActualValue.Contains(expectedValue, StringComparer.OrdinalIgnoreCase); + } + catch (FormatException) + { + formattedActualValue = ""; + isValid = false; + } + + errorMessage = string.Format(CultureInfo.CurrentCulture, "Expected: '{0}', Actual: '{1}'", FormattedExpectedValue, formattedActualValue); + return isValid; + } + + public override string FormatValue(string value) + { + return value; + } + } + } } diff --git a/src/WopiValidator.Core/Validators/ResponseHeaderValidator.cs b/src/WopiValidator.Core/Validators/ResponseHeaderValidator.cs index fa0eb7a..b07b402 100644 --- a/src/WopiValidator.Core/Validators/ResponseHeaderValidator.cs +++ b/src/WopiValidator.Core/Validators/ResponseHeaderValidator.cs @@ -17,14 +17,18 @@ class ResponseHeaderValidator : IValidator public readonly string ExpectedStateKey; public readonly bool IsRequired; public readonly bool ShouldMatch; + public readonly bool IsUrl; + public readonly bool IsExcluded; - public ResponseHeaderValidator(string key, string expectedValue, string expectedStateKey, bool isRequired = true, bool shouldMatch = true) + public ResponseHeaderValidator(string key, string expectedValue, string expectedStateKey, bool isRequired = true, bool shouldMatch = true, bool isUrl = false, bool isExcluded = false) { Key = key; DefaultExpectedValue = expectedValue; ExpectedStateKey = expectedStateKey; IsRequired = isRequired; ShouldMatch = shouldMatch; + IsUrl = isUrl; + IsExcluded = isExcluded; } public string Name @@ -38,13 +42,34 @@ public ValidationResult Validate(IResponseData data, IResourceManager resourceMa if (!data.Headers.TryGetValue(Key, out headerValue)) { - if (IsRequired) + if (IsExcluded || !IsRequired) { - return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "'{0}' header is not present on the response", Key)); + return new ValidationResult(); } else { - return new ValidationResult(); + return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "'{0}' header is not present on the response", Key)); + } + } + + if (IsExcluded) + { + return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "'{0}' header should not be present on the response", Key)); + } + + if (IsUrl) + { + if (string.IsNullOrEmpty(headerValue)) + { + return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "'{0}' header value should be any non empty string.", + Key)); + } + + Uri uri; + if (!Uri.TryCreate(headerValue, UriKind.Absolute, out uri)) + { + return new ValidationResult(string.Format(CultureInfo.CurrentCulture, "'{0}' header value should be a valid url.", + Key)); } } diff --git a/src/WopiValidator.Core/WopiDiscovery.cs b/src/WopiValidator.Core/WopiDiscovery.cs new file mode 100644 index 0000000..c7f9b10 --- /dev/null +++ b/src/WopiValidator.Core/WopiDiscovery.cs @@ -0,0 +1,594 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System.Xml.Serialization; + +// +// This source code was auto-generated by xsd, Version=4.6.1055.0. +// + +namespace Microsoft.Office.WopiValidator.Core +{ + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(TypeName = "ct_wopi-discovery")] + [System.Xml.Serialization.XmlRootAttribute("wopi-discovery", Namespace = "", IsNullable = false)] + public partial class ct_wopidiscovery + { + + private ct_netzone[] netzoneField; + + private ct_proofkey proofkeyField; + + /// + [System.Xml.Serialization.XmlElementAttribute("net-zone")] + public ct_netzone[] netzone + { + get + { + return this.netzoneField; + } + set + { + this.netzoneField = value; + } + } + + /// + [System.Xml.Serialization.XmlElementAttribute("proof-key")] + public ct_proofkey proofkey + { + get + { + return this.proofkeyField; + } + set + { + this.proofkeyField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(TypeName = "ct_net-zone")] + public partial class ct_netzone + { + + private ct_appname[] appField; + + private st_wopizone nameField; + + private bool nameFieldSpecified; + + /// + [System.Xml.Serialization.XmlElementAttribute("app")] + public ct_appname[] app + { + get + { + return this.appField; + } + set + { + this.appField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public st_wopizone name + { + get + { + return this.nameField; + } + set + { + this.nameField = value; + } + } + + /// + [System.Xml.Serialization.XmlIgnoreAttribute()] + public bool nameSpecified + { + get + { + return this.nameFieldSpecified; + } + set + { + this.nameFieldSpecified = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(TypeName = "ct_app-name")] + public partial class ct_appname + { + + private ct_wopiaction[] actionField; + + private string nameField; + + private string favIconUrlField; + + private bool checkLicenseField; + + public ct_appname() + { + this.checkLicenseField = false; + } + + /// + [System.Xml.Serialization.XmlElementAttribute("action")] + public ct_wopiaction[] action + { + get + { + return this.actionField; + } + set + { + this.actionField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string name + { + get + { + return this.nameField; + } + set + { + this.nameField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string favIconUrl + { + get + { + return this.favIconUrlField; + } + set + { + this.favIconUrlField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(false)] + public bool checkLicense + { + get + { + return this.checkLicenseField; + } + set + { + this.checkLicenseField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(TypeName = "ct_wopi-action")] + public partial class ct_wopiaction + { + + private st_wopiactionvalues nameField; + + private bool defaultField; + + private string requiresField; + + private string urlsrcField; + + private string extField; + + private string progidField; + + private string newprogidField; + + private string newextField; + + private bool useParentField; + + private string targetextField; + + public ct_wopiaction() + { + this.defaultField = false; + this.useParentField = false; + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public st_wopiactionvalues name + { + get + { + return this.nameField; + } + set + { + this.nameField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(false)] + public bool @default + { + get + { + return this.defaultField; + } + set + { + this.defaultField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string requires + { + get + { + return this.requiresField; + } + set + { + this.requiresField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string urlsrc + { + get + { + return this.urlsrcField; + } + set + { + this.urlsrcField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string ext + { + get + { + return this.extField; + } + set + { + this.extField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string progid + { + get + { + return this.progidField; + } + set + { + this.progidField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string newprogid + { + get + { + return this.newprogidField; + } + set + { + this.newprogidField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string newext + { + get + { + return this.newextField; + } + set + { + this.newextField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + [System.ComponentModel.DefaultValueAttribute(false)] + public bool useParent + { + get + { + return this.useParentField; + } + set + { + this.useParentField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string targetext + { + get + { + return this.targetextField; + } + set + { + this.targetextField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")] + [System.SerializableAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(TypeName = "st_wopi-action-values")] + public enum st_wopiactionvalues + { + + /// + view, + + /// + edit, + + /// + mobileView, + + /// + embedview, + + /// + embededit, + + /// + mobileclient, + + /// + present, + + /// + presentservice, + + /// + attend, + + /// + attendservice, + + /// + editnew, + + /// + imagepreview, + + /// + interactivepreview, + + /// + formsubmit, + + /// + formedit, + + /// + rest, + + /// + preloadview, + + /// + preloadedit, + + /// + rtc, + + /// + getinfo, + + /// + convert, + + /// + syndicate, + + /// + legacywebservice, + + /// + collab, + + /// + formpreview, + + /// + documentchat, + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(TypeName = "ct_proof-key")] + public partial class ct_proofkey + { + + private string exponentField; + + private string modulusField; + + private string oldexponentField; + + private string oldmodulusField; + + private string oldvalueField; + + private string valueField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string exponent + { + get + { + return this.exponentField; + } + set + { + this.exponentField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string modulus + { + get + { + return this.modulusField; + } + set + { + this.modulusField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string oldexponent + { + get + { + return this.oldexponentField; + } + set + { + this.oldexponentField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string oldmodulus + { + get + { + return this.oldmodulusField; + } + set + { + this.oldmodulusField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string oldvalue + { + get + { + return this.oldvalueField; + } + set + { + this.oldvalueField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string value + { + get + { + return this.valueField; + } + set + { + this.valueField = value; + } + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")] + [System.SerializableAttribute()] + [System.Xml.Serialization.XmlTypeAttribute(TypeName = "st_wopi-zone")] + public enum st_wopizone + { + + /// + [System.Xml.Serialization.XmlEnumAttribute("internal-http")] + internalhttp, + + /// + [System.Xml.Serialization.XmlEnumAttribute("internal-https")] + internalhttps, + + /// + [System.Xml.Serialization.XmlEnumAttribute("external-http")] + externalhttp, + + /// + [System.Xml.Serialization.XmlEnumAttribute("external-https")] + externalhttps, + } +} diff --git a/src/WopiValidator.Core/WopiValidator.Core.csproj b/src/WopiValidator.Core/WopiValidator.Core.csproj index e70376f..b85ec87 100644 --- a/src/WopiValidator.Core/WopiValidator.Core.csproj +++ b/src/WopiValidator.Core/WopiValidator.Core.csproj @@ -16,10 +16,16 @@ + + + + + + diff --git a/src/WopiValidator/DiscoveryOptions.cs b/src/WopiValidator/DiscoveryOptions.cs new file mode 100644 index 0000000..f19a05c --- /dev/null +++ b/src/WopiValidator/DiscoveryOptions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CommandLine; +using Microsoft.Office.WopiValidator.Core; +using System; + +namespace Microsoft.Office.WopiValidator +{ + /// + /// Options for the discovery command. + /// + [Verb("discovery", HelpText = "Provide XML that describes the supported abilities of this WOPI client")] + internal class DiscoveryOptions : OptionsBase + { + [Option("port", Required = true, HelpText = "Port number used for discovery")] + public string Port { get; set; } + + [Option("progid", Required = false, HelpText = "progid that identifies a folder as being associated with a specific application")] + public string ProgId { get; set; } + + [Option('p', "ProofKey", Required = true, HelpText = "Public key used to decrypt X-WOPI-Proof HTTP header")] + public string ProofKey { get; set; } + + [Option('o', "ProofKeyOld", Required = true, HelpText = "Public key used to decrypt X-WOPI-ProofOld HTTP header")] + public string ProofKeyOld { get; set; } + + public static ExitCode DiscoveryCommand(DiscoveryOptions options) + { + int port; + if (!Int32.TryParse(options.Port, out port)) + { + throw new ArgumentException(string.Format("Value for argument 'port' must be an integer, actual value '{0}'.", options.Port)); + } + + DiscoveryListener listener = new DiscoveryListener(options.ProofKey, options.ProofKeyOld, port); + listener.Start(); + + return ExitCode.Success; + } + } +} diff --git a/src/WopiValidator/Helpers.cs b/src/WopiValidator/Helpers.cs new file mode 100644 index 0000000..dd16d33 --- /dev/null +++ b/src/WopiValidator/Helpers.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Office.WopiValidator +{ + internal static class Helpers + { + internal static void WriteToConsole(string message, ConsoleColor color, int indentLevel = 0) + { + ConsoleColor currentColor = Console.ForegroundColor; + Console.ForegroundColor = color; + string indent = new string(' ', indentLevel * 2); + Console.Write(indent + message); + Console.ForegroundColor = currentColor; + } + + internal static bool ContainsAny(this HashSet set, params T[] items) + { + return set.Intersect(items).Any(); + } + + internal static string StripNewLines(this string str) + { + StringBuilder sb = new StringBuilder(str); + bool newLineAtStart = str.StartsWith(Environment.NewLine); + bool newLineAtEnd = str.EndsWith(Environment.NewLine); + sb.Replace(Environment.NewLine, " "); + + if (newLineAtStart) + { + sb.Insert(0, Environment.NewLine); + } + + if (newLineAtEnd) + { + sb.Append(Environment.NewLine); + } + return sb.ToString(); + } + + } +} diff --git a/src/WopiValidator/ListOptions.cs b/src/WopiValidator/ListOptions.cs new file mode 100644 index 0000000..1c976f3 --- /dev/null +++ b/src/WopiValidator/ListOptions.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CommandLine; +using Microsoft.Office.WopiValidator.Core; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Office.WopiValidator +{ + /// + /// Options for the list command. + /// + [Verb("list", HelpText = "List tests that match the filter criteria")] + internal class ListOptions : OptionsBase + { + [Option('t', "tags", Required = false, HelpText = "Filter to tests with these tags")] + public IEnumerable Tags { get; set; } + + internal static ExitCode ListCommand(ListOptions options) + { + // get run configuration from XML + IEnumerable testData = ConfigParser.ParseExecutionData(options.RunConfigurationFilePath); + + // Filter the tests + IEnumerable executionData = testData.ApplyFilters(options); + + // Create executor groups + var executorGroups = executionData.GroupBy(d => d.TestGroupName) + .Select(g => new + { + Name = g.Key, + TestData = g.Select(x => x) + }); + + + foreach (var group in executorGroups) + { + Helpers.WriteToConsole($"\nTest group: {group.Name}\n", ConsoleColor.White); + + foreach(var test in group.TestData) + { + Helpers.WriteToConsole($"{test.TestCase.Name}\n", ConsoleColor.Blue); + } + } + return ExitCode.Failure; + } + + //private static TestCaseExecutor GetTestCaseExecutor(TestExecutionData testExecutionData, ListOptions options, TestCategory inputTestCategory) + //{ + // bool officeNative = inputTestCategory == TestCategory.OfficeNativeClient || + // testExecutionData.TestCase.TestCategory == TestCategory.OfficeNativeClient; + // string userAgent = officeNative ? Constants.HeaderValues.OfficeNativeClientUserAgent : null; + + // return new TestCaseExecutor(testExecutionData, options.WopiEndpoint, options.AccessToken, options.AccessTokenTtl, userAgent); + //} + } +} diff --git a/src/WopiValidator/Options.cs b/src/WopiValidator/OptionsBase.cs similarity index 50% rename from src/WopiValidator/Options.cs rename to src/WopiValidator/OptionsBase.cs index 10494d5..0b3bc49 100644 --- a/src/WopiValidator/Options.cs +++ b/src/WopiValidator/OptionsBase.cs @@ -3,23 +3,17 @@ using CommandLine; using Microsoft.Office.WopiValidator.Core; +using System; +using System.Collections.Generic; +using System.Linq; namespace Microsoft.Office.WopiValidator { /// - /// Represents set of command line arguments that can be used to modify behavior of the application. + /// Options shared by all commands /// - class Options + internal abstract class OptionsBase : IFilterOptions { - [Option('w', "wopisrc", Required = true, HelpText = "WopiSrc URL for a wopitest file")] - public string WopiEndpoint { get; set; } - - [Option('t', "token", Required = true, HelpText = "WOPI access token")] - public string AccessToken { get; set; } - - [Option('l', "token_ttl", Required = true, HelpText = "WOPI access token ttl")] - public long AccessTokenTtl { get; set; } - [Option('c', "config", Required = false, Default = "TestCases.xml", HelpText = "Path to XML file with test definitions")] public string RunConfigurationFilePath { get; set; } @@ -29,10 +23,20 @@ class Options [Option('n', "testname", Required = false, HelpText = "Run only the test specified (cannot be used with testgroup)")] public string TestName { get; set; } - [Option('e', "testcategory", Required = false, Default = TestCategory.All, HelpText = "Run only the tests in the specified category")] + [Option('e', "testcategory", Required = false, Default = Core.TestCategory.All, HelpText = "Run only the tests in the specified category")] public TestCategory TestCategory { get; set; } - [Option('s', "ignore-skipped", Required = false, HelpText = "Don't output any info about skipped tests.")] - public bool IgnoreSkipped { get; set; } + TestCategory? IFilterOptions.TestCategory + { + get { return TestCategory; } + set + { + if (!value.HasValue) + { + TestCategory = TestCategory.All; + } + TestCategory = value.Value; + } + } } } diff --git a/src/WopiValidator/Program.cs b/src/WopiValidator/Program.cs index f879939..24011c6 100644 --- a/src/WopiValidator/Program.cs +++ b/src/WopiValidator/Program.cs @@ -2,14 +2,8 @@ // Licensed under the MIT License. using CommandLine; -using Microsoft.Office.WopiValidator.Core; using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Text; namespace Microsoft.Office.WopiValidator { @@ -21,19 +15,6 @@ internal enum ExitCode internal class Program { - private static TestCaseExecutor GetTestCaseExecutor(TestExecutionData testExecutionData, Options options, TestCategory inputTestCategory) - { - TestCategory testCategory; - if (!Enum.TryParse(testExecutionData.TestCase.Category, true /* ignoreCase */, out testCategory)) - { - throw new Exception(string.Format(CultureInfo.InvariantCulture, "Invalid TestCategory for TestCase : {0}", testExecutionData.TestCase.Name)); - } - - string userAgent = (inputTestCategory == TestCategory.OfficeNativeClient || testCategory == TestCategory.OfficeNativeClient) ? Constants.HeaderValues.OfficeNativeClientUserAgent : null; - - return new TestCaseExecutor(testExecutionData, options.WopiEndpoint, options.AccessToken, options.AccessTokenTtl, userAgent); - } - private static int Main(string[] args) { // Wrapping all logic in a top-level Exception handler to ensure that exceptions are @@ -41,165 +22,25 @@ private static int Main(string[] args) ExitCode exitCode = ExitCode.Success; try { - exitCode = Parser.Default.ParseArguments(args) - .MapResult( - (Options options) => Execute(options), - parseErrors => ExitCode.Failure); + exitCode = Parser.Default.ParseArguments(args) + .MapResult( + (RunOptions options) => RunOptions.RunCommand(options), + (ListOptions options) => ListOptions.ListCommand(options), + (DiscoveryOptions options) => DiscoveryOptions.DiscoveryCommand(options), + parseErrors => ExitCode.Failure); } catch (Exception ex) { - WriteToConsole(ex.ToString(), ConsoleColor.Red); + Helpers.WriteToConsole(ex.ToString(), ConsoleColor.Red); exitCode = ExitCode.Failure; } if (Debugger.IsAttached) { - WriteToConsole("Press any key to exit", ConsoleColor.White); + Helpers.WriteToConsole("Press any key to exit", ConsoleColor.White); Console.ReadLine(); } return (int)exitCode; } - - private static ExitCode Execute(Options options) - { - // get run configuration from XML - IEnumerable testData = ConfigParser.ParseExecutionData(options.RunConfigurationFilePath, options.TestCategory); - - if (!String.IsNullOrEmpty(options.TestGroup)) - { - testData = testData.Where(d => d.TestGroupName == options.TestGroup); - } - - IEnumerable executionData; - if (!String.IsNullOrWhiteSpace(options.TestName)) - { - executionData = new TestExecutionData[] { TestExecutionData.GetDataForSpecificTest(testData, options.TestName) }; - } - else - { - executionData = testData; - } - - // Create executor groups - var executorGroups = executionData.GroupBy(d => d.TestGroupName) - .Select(g => new - { - Name = g.Key, - Executors = g.Select(x => GetTestCaseExecutor(x, options, options.TestCategory)) - }); - - ConsoleColor baseColor = ConsoleColor.White; - HashSet resultStatuses = new HashSet(); - foreach (var group in executorGroups) - { - WriteToConsole($"\nTest group: {group.Name}\n", ConsoleColor.White); - - // define execution query - evaluation is lazy; test cases are executed one at a time - // as you iterate over returned collection - var results = group.Executors.Select(x => x.Execute()); - - // iterate over results and print success/failure indicators into console - foreach (TestCaseResult testCaseResult in results) - { - resultStatuses.Add(testCaseResult.Status); - switch (testCaseResult.Status) - { - case ResultStatus.Pass: - baseColor = ConsoleColor.Green; - WriteToConsole($"Pass: {testCaseResult.Name}\n", baseColor, 1); - break; - - case ResultStatus.Skipped: - baseColor = ConsoleColor.Yellow; - if (!options.IgnoreSkipped) - { - WriteToConsole($"Skipped: {testCaseResult.Name}\n", baseColor, 1); - } - break; - - case ResultStatus.Fail: - default: - baseColor = ConsoleColor.Red; - WriteToConsole($"Fail: {testCaseResult.Name}\n", baseColor, 1); - break; - } - - if (testCaseResult.Status == ResultStatus.Fail || - (testCaseResult.Status == ResultStatus.Skipped && !options.IgnoreSkipped)) - { - foreach (var request in testCaseResult.RequestDetails) - { - var responseStatus = (HttpStatusCode)request.ResponseStatusCode; - var color = request.ValidationFailures.Count == 0 ? ConsoleColor.DarkGreen : baseColor; - WriteToConsole($"{request.Name}, response code: {request.ResponseStatusCode} {responseStatus}\n", color, 2); - foreach (var failure in request.ValidationFailures) - { - foreach (var error in failure.Errors) - WriteToConsole($"{error.StripNewLines()}\n", baseColor, 3); - } - } - - WriteToConsole($"Re-run command: .\\wopivalidator.exe -n {testCaseResult.Name} -w {options.WopiEndpoint} -t {options.AccessToken} -l {options.AccessTokenTtl}\n", baseColor, 2); - Console.WriteLine(); - } - } - - if (options.IgnoreSkipped && !resultStatuses.ContainsAny(ResultStatus.Pass, ResultStatus.Fail)) - { - WriteToConsole($"All tests skipped.\n", baseColor, 1); - } - } - - // If skipped tests are ignored, don't consider them when determining whether the test run passed or failed - if (options.IgnoreSkipped) - { - if (resultStatuses.Contains(ResultStatus.Fail)) - { - return ExitCode.Failure; - } - } - // Otherwise consider skipped tests as failures - else if (resultStatuses.ContainsAny(ResultStatus.Skipped, ResultStatus.Fail)) - { - return ExitCode.Failure; - } - return ExitCode.Success; - } - - private static void WriteToConsole(string message, ConsoleColor color, int indentLevel = 0) - { - ConsoleColor currentColor = Console.ForegroundColor; - Console.ForegroundColor = color; - string indent = new string(' ', indentLevel * 2); - Console.Write(indent + message); - Console.ForegroundColor = currentColor; - } - } - - internal static class ExtensionMethods - { - internal static bool ContainsAny(this HashSet set, params T[] items) - { - return set.Intersect(items).Any(); - } - - internal static string StripNewLines(this string str) - { - StringBuilder sb = new StringBuilder(str); - bool newLineAtStart = str.StartsWith(Environment.NewLine); - bool newLineAtEnd = str.EndsWith(Environment.NewLine); - sb.Replace(Environment.NewLine, " "); - - if (newLineAtStart) - { - sb.Insert(0, Environment.NewLine); - } - - if (newLineAtEnd) - { - sb.Append(Environment.NewLine); - } - return sb.ToString(); - } } } diff --git a/src/WopiValidator/RunOptions.cs b/src/WopiValidator/RunOptions.cs new file mode 100644 index 0000000..66aebc8 --- /dev/null +++ b/src/WopiValidator/RunOptions.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using CommandLine; +using Microsoft.Office.WopiValidator.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Globalization; +using System.Text; + +namespace Microsoft.Office.WopiValidator +{ + /// + /// Represents set of command line arguments that can be used to modify behavior of the application. + /// + [Verb("run", HelpText = "Run tests.")] + internal class RunOptions : OptionsBase + { + [Option('w', "wopisrc", Required = true, HelpText = "WopiSrc URL for a wopitest file")] + public string WopiEndpoint { get; set; } + + [Option('t', "token", Required = true, HelpText = "WOPI access token")] + public string AccessToken { get; set; } + + [Option('l', "token_ttl", Required = true, HelpText = "WOPI access token ttl")] + public long AccessTokenTtl { get; set; } + + [Option("UsingRestrictedScenario", Required = false, HelpText = "Header 'X-WOPI-UsingRestrictedScenario' used Restricted scenario")] + public string UsingRestrictedScenario { get; set; } + + [Option("ApplicationId", Required = false, HelpText = "Header 'X-WOPI-ApplicationId' indicates id of an application stored in secure store")] + public string ApplicationId { get; set; } + + [Option("RSACryptoKeyPairValue", Required = false, HelpText = "key-pairs match the Asymmetric encrypt algorithm used for X-WOPI-Proof header")] + public string RSACryptoKeyPairValue { get; set; } + + [Option("RSACryptoKeyPairOldValue", Required = false, HelpText = "key-pairs match the Asymmetric encrypt algorithm used for X-WOPI-ProofOld header")] + public string RSACryptoKeyPairOldValue { get; set; } + + [Option('s', "ignore-skipped", Required = false, HelpText = "Don't output any info about skipped tests.")] + public bool IgnoreSkipped { get; set; } + + public static ExitCode RunCommand(RunOptions options) + { + // get run configuration from XML + IEnumerable testData = ConfigParser.ParseExecutionData(options.RunConfigurationFilePath, options.ApplicationId, options.UsingRestrictedScenario); + + // Filter the tests + IEnumerable executionData = testData.ApplyFilters(options); + + RSACryptoServiceProvider rsaProvider = null; + RSACryptoServiceProvider rsaProviderOld = null; + + if (!string.IsNullOrEmpty(options.RSACryptoKeyPairValue) && !string.IsNullOrEmpty(options.RSACryptoKeyPairOldValue)) + { + rsaProvider = new RSACryptoServiceProvider(); + rsaProvider.ImportCspBlob(Convert.FromBase64String(options.RSACryptoKeyPairValue)); + + rsaProviderOld = new RSACryptoServiceProvider(); + rsaProviderOld.ImportCspBlob(Convert.FromBase64String(options.RSACryptoKeyPairOldValue)); + } + + // Create executor groups + var executorGroups = executionData.GroupBy(d => d.TestGroupName) + .Select(g => new + { + Name = g.Key, + Executors = g.Select(x => GetTestCaseExecutor(x, options, options.TestCategory, rsaProvider, rsaProviderOld)) + }); + + ConsoleColor baseColor = ConsoleColor.White; + HashSet resultStatuses = new HashSet(); + foreach (var group in executorGroups) + { + Helpers.WriteToConsole($"\nTest group: {group.Name}\n", ConsoleColor.White); + + // define execution query - evaluation is lazy; test cases are executed one at a time + // as you iterate over returned collection + var results = group.Executors.Select(x => x.Execute()); + + // iterate over results and print success/failure indicators into console + foreach (TestCaseResult testCaseResult in results) + { + resultStatuses.Add(testCaseResult.Status); + switch (testCaseResult.Status) + { + case ResultStatus.Pass: + baseColor = ConsoleColor.Green; + Helpers.WriteToConsole($"Pass: {testCaseResult.Name}\n", baseColor, 1); + break; + + case ResultStatus.Skipped: + baseColor = ConsoleColor.Yellow; + if (!options.IgnoreSkipped) + { + Helpers.WriteToConsole($"Skipped: {testCaseResult.Name}\n", baseColor, 1); + } + break; + + case ResultStatus.Fail: + default: + baseColor = ConsoleColor.Red; + Helpers.WriteToConsole($"Fail: {testCaseResult.Name}\n", baseColor, 1); + break; + } + + if (testCaseResult.Status == ResultStatus.Fail || + (testCaseResult.Status == ResultStatus.Skipped && !options.IgnoreSkipped)) + { + foreach (var request in testCaseResult.RequestDetails) + { + var responseStatus = (HttpStatusCode)request.ResponseStatusCode; + var color = request.ValidationFailures.Count == 0 ? ConsoleColor.DarkGreen : baseColor; + Helpers.WriteToConsole($"{request.Name}, response code: {request.ResponseStatusCode} {responseStatus}\n", color, 2); + foreach (var failure in request.ValidationFailures) + { + foreach (var error in failure.Errors) + Helpers.WriteToConsole($"{error.StripNewLines()}\n", baseColor, 3); + } + } + + Helpers.WriteToConsole($"Re-run command: .\\wopivalidator.exe -n {testCaseResult.Name} -w {options.WopiEndpoint} -t {options.AccessToken} -l {options.AccessTokenTtl}\n", baseColor, 2); + Console.WriteLine(); + } + } + + if (options.IgnoreSkipped && !resultStatuses.ContainsAny(ResultStatus.Pass, ResultStatus.Fail)) + { + Helpers.WriteToConsole($"All tests skipped.\n", baseColor, 1); + } + } + + // If skipped tests are ignored, don't consider them when determining whether the test run passed or failed + if (options.IgnoreSkipped) + { + if (resultStatuses.Contains(ResultStatus.Fail)) + { + return ExitCode.Failure; + } + } + // Otherwise consider skipped tests as failures + else if (resultStatuses.ContainsAny(ResultStatus.Skipped, ResultStatus.Fail)) + { + return ExitCode.Failure; + } + return ExitCode.Success; + } + + private static TestCaseExecutor GetTestCaseExecutor(TestExecutionData testExecutionData, RunOptions options, TestCategory inputTestCategory, RSACryptoServiceProvider rsaProvider, RSACryptoServiceProvider rsaProviderOld) + { + bool officeNative = inputTestCategory == TestCategory.OfficeNativeClient || + testExecutionData.TestCase.TestCategory == TestCategory.OfficeNativeClient; + string userAgent = officeNative ? Constants.HeaderValues.OfficeNativeClientUserAgent : null; + + return new TestCaseExecutor(testExecutionData, options.WopiEndpoint, options.AccessToken, options.AccessTokenTtl, userAgent, rsaProvider, rsaProviderOld); + } + } +} diff --git a/src/WopiValidator/WopiValidator.csproj b/src/WopiValidator/WopiValidator.csproj index 65e30a4..3c6ca25 100644 --- a/src/WopiValidator/WopiValidator.csproj +++ b/src/WopiValidator/WopiValidator.csproj @@ -43,6 +43,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest