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