From f73b97bb77645a9baa662a86543db3fed497b612 Mon Sep 17 00:00:00 2001 From: Longjia Niu Date: Thu, 18 Sep 2025 15:58:29 -0700 Subject: [PATCH] test copilot --- COPILOT_AGENT.md | 78 + TestCases.xml | 7117 +---------------- WopiValidator.sln | 1 + src/WopiValidator.Core/Constants.cs | 1 + .../JsonSchemas/CheckFileInfoSchema.json | 49 +- .../CheckFileInfoSchemaCsppPlus.json | 421 + .../Validators/JsonSchemaValidator.cs | 117 +- .../JsonSchemaValidatorUnitTests.cs | 255 + 8 files changed, 927 insertions(+), 7112 deletions(-) create mode 100644 COPILOT_AGENT.md create mode 100644 src/WopiValidator.Core/JsonSchemas/CheckFileInfoSchemaCsppPlus.json diff --git a/COPILOT_AGENT.md b/COPILOT_AGENT.md new file mode 100644 index 0000000..16d9787 --- /dev/null +++ b/COPILOT_AGENT.md @@ -0,0 +1,78 @@ +## Online Resources +### CheckFileInfo properties documented in Microsoft-365 documents +* https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-csppp +* https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo?source=recommendations +* https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-other + +## Updates Made to WOPI Validator + +### 1. Enhanced JsonSchemaValidator for Comprehensive CheckFileInfo Validation + +The `JsonSchemaValidator` has been significantly enhanced to provide comprehensive validation for all CheckFileInfo properties according to Microsoft's WOPI specification: + +**Key Features:** +- **Timestamp Validation**: Validates Unix timestamp properties (`AccessTokenExpiry`, `ServerTime`) to ensure they are within reasonable bounds (current time ± 10 years) +- **Format Validation**: Enforces proper format requirements for properties like `FileExtension` (must start with '.'), `SHA256` (64-character hexadecimal), and URLs +- **Required Property Validation**: Ensures all required properties (`BaseFileName`, `OwnerId`, `Size`, `UserId`, `Version`) are present and non-empty +- **CSPP vs CSPP+ Validation**: Supports different validation rules for Cloud Storage Partner Program and Cloud Storage Partner Program Plus +- **Comprehensive Error Messages**: Provides detailed error messages with specific validation failures and expected ranges + +**Timestamp Boundary Validation:** +- For `AccessTokenExpiry` and `ServerTime` properties, the validator now checks that Unix timestamp values are within a 20-year window (10 years before and after current time) +- Invalid timestamps result in descriptive error messages indicating the valid range and the actual timestamp value converted to human-readable format + +### 2. Updated CheckFileInfoSchema.json + +The JSON schema has been comprehensively updated to include: +- All possible CheckFileInfo properties from the Microsoft documentation +- Proper format constraints (e.g., `FileExtension` pattern, `SHA256` pattern, URL formats) +- Required vs optional property specifications +- Type validation with appropriate constraints (e.g., minimum values for `Size`, `SequenceNumber`) +- Enhanced descriptions for critical properties + +**New Properties Added:** +- `SupportsChunkedFileTransfer` for incremental file transfer +- Enhanced validation patterns for existing properties +- Better constraint definitions for numerical properties + +### 3. Updated TestCases.xml + +All test cases that use `CheckFileInfo` requests now include the `JsonSchemaValidator` with the updated schema: +- **Prerequisites**: All prerequisite test cases now validate the complete CheckFileInfo schema +- **Test Groups**: Schema validation is applied consistently across all test groups +- **Error Detection**: Improved error detection for malformed CheckFileInfo responses + +### 4. Enhanced Unit Tests + +New comprehensive unit tests have been added to validate: +- **Timestamp Validation**: Tests for valid and invalid Unix timestamps +- **Format Validation**: Tests for proper format requirements (FileExtension, SHA256, etc.) +- **Boundary Conditions**: Tests for edge cases like negative sizes, empty required fields +- **Schema Compliance**: Tests to ensure all required and optional properties are handled correctly + +### 5. Benefits for WOPI Implementers + +**Improved Validation Coverage:** +- Catches more validation errors early in the development cycle +- Provides specific guidance on what needs to be fixed +- Ensures compliance with Microsoft's latest WOPI specifications + +**Better Error Messages:** +- Clear indication of which properties failed validation +- Specific ranges and expected formats provided +- Human-readable timestamp conversion for debugging + +**CSPP+ Readiness:** +- Validates properties required for Cloud Storage Partner Program Plus +- Ensures compatibility with collaborative editing features +- Validates timestamp properties critical for token management + +### Usage + +The enhanced validation is automatically applied to all `CheckFileInfo` requests in the test suite. No changes are required for existing WOPI implementations, but they will now receive more comprehensive validation feedback. + +For hosts implementing CSPP+, ensure that: +1. `AccessTokenExpiry` timestamps are valid Unix timestamps within the expected range +2. `UserFriendlyName` is provided when `SupportsCoauth` is true +3. All URL properties use proper absolute URL formats +4. Timestamp properties like `ServerTime` reflect current server time accurately diff --git a/TestCases.xml b/TestCases.xml index 5ffe2d3..f243811 100644 --- a/TestCases.xml +++ b/TestCases.xml @@ -34,6 +34,7 @@ + @@ -49,6 +50,7 @@ + @@ -64,6 +66,7 @@ + @@ -79,6 +82,7 @@ + @@ -94,6 +98,7 @@ + @@ -111,6 +116,7 @@ + @@ -126,6 +132,7 @@ + @@ -142,6 +149,7 @@ + @@ -157,6 +165,7 @@ + @@ -172,6 +181,7 @@ + @@ -187,6 +197,7 @@ + @@ -202,6 +213,7 @@ + @@ -217,6 +229,7 @@ + @@ -232,6 +245,7 @@ + @@ -247,6 +261,7 @@ + @@ -262,6 +277,7 @@ + @@ -277,6 +293,7 @@ + @@ -355,6 +372,7 @@ + @@ -391,6 +409,7 @@ + @@ -426,6 +445,7 @@ + @@ -441,6 +461,7 @@ + @@ -471,6 +492,9 @@ + + + - - - Simulates creating a new file on the host, which requires PutFile requests to an unlocked 0-byte file to succeed. - - - - - - - - - - - - - - - - - - - - - - - - This tests that PutFile requests fail when executed against files that are not zero bytes. - - - - - - - - - - - - - - - - - - - - - - - Simulates Get and PutFile requests with invalid access token and expects a 401 or 404 response. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - LocksPrereq - - - - - - - /files/GetFile should return the current version of the file - - - - - - - - - - - - - - /files/Lock should return the current version of the file - - - - - - - - - - - - - - - - - /files/Unlock should return the current version of the file - - - - - - - - - - - - - - - - - - /files/PutFile should return the current version of the file - - - - - - - - - - - - - - - - - - - Performs consecutive PutFile operations to verify that the X-WOPI-ItemVersion header value is changed with every PutFile operation. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Performs Lock and Unlock operations after a GetFile operation to verify that the X-WOPI-ItemVersion header value is unchanged. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - SupportsUserInfoPrereq - - - - - - - PutUserInfo call returns 200 and subsequent CheckFileInfo calls return the correct value - - - - PutUserInfoTest - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - FileEditingPrereq - UserCanWriteRelativePrereq - LocksPrereq - DeleteFilePrereq - - - - - 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 both a suggested name and a relative name are specified. - - - - - - - - - - - - - - - - - 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 the host returns a UTF-8 encoded version of the FileName after a PutRelativeFile operation. - - - - - - - - - - - - - - - - - - - - - - Tests that the host handles long file names appropriately by either supporting them or returning a 400. - - - - - - - - - - - - - - - - - - - - - - - Tests that the host returns the HostEditUrl and HostViewUrl on their PutRelativeFile response. - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - FileEditingPrereq - LocksPrereq - UserCanNotWriteRelativePrereq - - - - - 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 Relative name is specified. - - - - - - - - - - - - - - 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 and OverwriteRelative is set to false. - - - - - - - - - - - - - - Tests the PutRelativeFile scenario where both a suggested name and a relative name are specified. - - - - - - - - - - - - - - - - - WopiValidatorPrereq - FileEditingPrereq - LocksPrereq - DeleteFilePrereq - ContainersUnsupportedPrereq - RenameFilePrereq - UserCanWriteRelativePrereq - - - - - Perform a PutRelativeFile operation, then rename and delete it. The rename operation should succeed. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Perform a PutRelativeFile operation, then rename it with a file name containing a special character and delete it. - The rename operation should succeed and the server should return the name as a UTF-8 encoded string. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Perform a PutRelativeFile operation, then lock it and try to rename it with the correct lock ID. The rename operation should succeed. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Perform a PutRelativeFile operation, then lock it and try to rename it with an incorrect lock ID. - The rename operation should fail with a Conflict - 409 status code. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Perform a PutRelativeFile operation, then delete it and try to rename it. The rename operation should fail with a 404 status code. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Perform a PutRelativeFile operation, then rename and delete it. The rename operation should not change the file extension of the file. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - FileEditingPrereq - LocksPrereq - ContainersPrereq - DeleteFilePrereq - RenameFilePrereq - UserCanCreateChildFilePrereq - - - - - Create a file, then rename and delete it. The rename operation should succeed. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create a file, then rename it with a file name containing a special character and delete it. - The rename operation should succeed and the server should return the name as a UTF-8 encoded string. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create a file, then lock it and try to rename it with the correct lock ID. The rename operation should succeed. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create a file, then lock it and try to rename it with an incorrect lock ID. - The rename operation should fail with a Conflict - 409 status code. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create a file, then delete it and try to rename it. The rename operation should fail with a 404 status code. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create a file, then rename and delete it. The rename operation should not change the file extension of the file. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - ContainersPrereq - EcosystemPrereq - - - - - - Tests that GetEcosystem returns a valid response. - - - - - - - - - - - - - - - - Tests that CheckEcosystem returns a valid response. - - - - - - - - - - - - - - - - - - - - - - - - Tests that GetRootContainer returns a valid response. - - - - - - - - - - - - - - - - - - - - - Tests that GetRootContainer denies requests with invalid access tokens. - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - UserCanWritePrereq - ContainersPrereq - - - - - - Tests that CheckContainerInfo returns a valid response. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests creating a child container and deleting it. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that CreateChildContainer handles 'suggested mode' properly. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that CreateChildContainer handles 'specific mode' properly. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that CreateChildContainer handles conflicting container names when in 'specific mode' properly. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /files/EnumerateAncestors then /containers/GetEcosystem on each element - - - - - - - - - - - - - - - - - - - - Verify that some CheckContainerInfo user properties match between a child container and its parent. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - UserCanWritePrereq - ContainersPrereq - UserCanRenameContainerPrereq - - - - - - Tests that RenameContainer is handled properly. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.RenameChildContainer scenario where a Relative name is specified. Expect the host to rename - the container with the exact name as specified. Also ensure that the host returns a UTF-8 encoded version of the ContainerName. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - ContainersPrereq - - - - - just EnumerateAncestors - - - - - - - - - - - - - - - /files/EnumerateAncestors then /containers/EnumerateChildren on the parent folder - - - - - - - - - - - - - - - - - - - - - /files/EnumerateAncestors then /containers/EnumerateChildren on the root folder - - - - - - - - - - - - - - /files/EnumerateAncestors then /containers/EnumerateChildren on the parent folder with a file extension filter - - - - - - - - - - - - - - - - - - - - - /files/EnumerateAncestors then /containers/EnumerateChildren on the parent folder with multiple file extensions filtered - - - - - - - - - - - - - - - - - - - - - - /files/EnumerateAncestors then /containers/EnumerateAncestors on a container which is a root container - - - - - - - - - - - - - - - - - /files/EnumerateAncestors then /containers/EnumerateAncestors on a container which is not a root container - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - UserCanWritePrereq - ContainersPrereq - DeleteFilePrereq - - - - - - - /files/EnumerateAncestors then go to a container that has UserCanCreateChildFile=true. - create a file. delete it. - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile scenario where a suggested name is specified. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile scenario where a suggested name is specified but - a file with the target name already exists. Expect the request to succeed with the host - choosing a suitable name. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile scenario where a Relative name is specified. Expect the host to create - a new file with the exact name as specified. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile 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. Expect the host to create a new file with the exact name as specified. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile 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. Expect the host to create a new file with the exact name as specified. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile scenario where a Relative name is specified and OverwriteRelative - is not specified. Since a file with target name exists in this scenario, expect the host to - return 409 status code as conflict. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile scenario where a Relative name is specified and OverwriteRelative - is set to false. Since a file with target name exists in this scenario, expect the host to - return 409 status code as conflict. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile scenario where a Relative name is specified and OverwriteRelative - is set to true. Since a file with target name exists in this scenario, expect the host to - succeed by choosing a suitable name. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile scenario where both a suggested name and a relative name are specified. - Expect the host to fail the request as bad request. - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile 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. Expect the host - to fail the request with a 409 conflict code. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile scenario where a Relative name is specified. Expect the host to create - a new file with the exact name as specified. Also ensure that the host returns a UTF-8 encoded version of the FileName. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the Containers.CreateChildFile scenario where a file name is longer than 512 characters, then the host should return a 400 or 200. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Verify that some CheckContainerInfo and CheckFileInfo user properties match between a child file and its parent container. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - UserCanWritePrereq - FileEditingPrereq - LocksPrereq - FileUrlUsagePrereq - - - - - Tests that the FileUrl returns updated content after a PutFile request, since FileUrl is intended to be a drop in replacement for GetFile. - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - FileUrlUsagePrereq - - - - - Use FileUrl, if provided, to directly retrieve the contents of a file instead of using GetFile. - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - ShareUrlTypeReadOnlyForFilePrereq - - - - - Tests the GetShareUrl operation for a file where the requested share url type is "ReadOnly". - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - ShareUrlTypeReadWriteForFilePrereq - - - - - Tests the GetShareUrl operation for a file where the requested share url type is "ReadWrite". - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - SupportedShareUrlTypesForFilePrereq - - - - - Tests the GetShareUrl operation for a file where the requested share url type is unknown. - - - - - - - - - - - - - - - - WopiValidatorPrereq - ContainersPrereq - ShareUrlTypeReadOnlyForContainerPrereq - - - - - Tests the GetShareUrl operation for a container where the requested share url type is "ReadOnly". - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - ContainersPrereq - ShareUrlTypeReadWriteForContainerPrereq - - - - - Tests the GetShareUrl operation for a container where the requested share url type is "ReadWrite". - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - ContainersPrereq - SupportedShareUrlTypesForContainerPrereq - - - - - Tests the GetShareUrl operation for a container where the requested share url type is unknown. - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - FileEditingPrereq - AddActivitiesPrereq - - - - - one comment, minimal properties - - - - - - - - - - - - - - - - - - - - - - - - one comment with all data properties set - - - - - - - - - - - - - - - - - - - - - - - - one comment with all-caps ID GUID - - - - - - - - - - - - - - - - - - - - - - - - one comment with max-length ContentID - - - - - - - - - - - - - - - - - - - - - - - - one comment with max-length NavigationId - - - - - - - - - - - - - - - - - - - - - - - - one comment with ContentAction=update - - - - - - - - - - - - - - - - - - - - - - - - one comment with ContentAction=delete - - - - - - - - - - - - - - - - - - - - - - - - one comment with bogus additional toplevel property - - - - - - - - - - - - - - - - - - - - - - - - one comment with bogus additional data property - - - - - - - - - - - - - - - - - - - - - - - - one comment, one person - - - - - - - - - - - - - - - - - - - - - - - - one comment, three people - - - - - - - - - - - - - - - - - - - - - - - - one comment, one person with bogus additional property - - - - - - - - - - - - - - - - - - - - - - - - one comment, one person with non-wopi provider and ID - - - - - - - - - - - - - - - - - - - - - - - - one activity with bogus type - - - - - - - - - - - - - - - - - - - one activity with bogus type and no data - - - - - - - - - - - - - - - - - - - - - - - - one comment and one activity with bogus type - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - three comments - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - - - - - Tests that hosts accept requests where the X-WOPI-Proof value is correctly signed with the current proof key, - and the X-WOPI-ProofOld value is signed with the old proof key. - - - - - - - - - Tests that hosts accept requests where the X-WOPI-Proof value is correctly signed with the current proof key, - but the X-WOPI-ProofOld value is invalid. This scenario is unusual and should not happen in a production - environment, but since the X-WOPI-Proof value is signed with the current public key, the request should be - accepted. - - - - - - - - - - - - - Tests that hosts accept requests where the X-WOPI-Proof value is invalid but the X-WOPI-ProofOld value is - signed with current public key. This can happen when a WOPI client such as Office Online has rotated proof keys - but the host hasn't re-run WOPI discovery yet. - - - - - - - - - - - - - Tests that hosts accept requests where the X-WOPI-ProofOld value is invalid but the X-WOPI-Proof value is - signed with old public key. This can happen when a WOPI client has rotated proof keys, the host has re-run - WOPI discovery and has the updated keys, but the datacenter machine making the WOPI request does not yet have - the updated keys. - - - - - - - - - - - - - Tests that hosts reject requests where the X-WOPI-Proof value is invalid, and the X-WOPI-ProofOld value - is signed with the old public key. This scenario is unusual and should not happen in a production - environment; such requests should be rejected. - - - - - - - - - - - - - - - - Tests that hosts reject requests with invalid current and old proof keys. - - - - - - - - - - - - - - - - Tests that hosts reject requests with an X-WOPI-Timestamp value that represents a time more than 20 minutes old. - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - SupportsCoauthPrereq - - - - - Tests that a single coauth lock is requested and is reflected in the coauth table. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initiates GetCoauthTable request with the same CoathTableVersion as host, host returns no response body. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock's expiration timeout is refreshed when requested with the same coauth lock id as an already existing coauth lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth exclusive lock request replaces the lock type of a coauth lock of the same id. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that an unlock coauth lock request unlocks the coauth lock from the coauth table. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that multiple coauth locks are requested and are reflected in the coauth table. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that multiple coauth locks are requested, then GetCoauthTable with different CoauthTableVersion from host, CoauthTable will be returned. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that multiple coauth locks are requested, then GetCoauthTable with same CoauthTableVersion as host, no response body returned. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that multiple coauth locks and coauth unlocks are requested and are reflected in the coauth table. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that multiple coauth locks of different types can exist in the coauth table. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that multiple coauth locks of different types exist on the coauth table, then GetCoauthTable requested with same CoauthTableVersion as host, no response body returned. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that the coauth table request will return an empty array if there aren't any coauth locks. - - - - - - - - - - - - - - - - - - Tests when a coauth lock is requested with the same coauth lock id as an existing coauth exclusive lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests when a coauth lock is requested with a different coauth lock id when a coauth exclusive lock already exists. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that the response of the coauth lock metadata is set from the passed metadata header. - - - - - - - - - - - - - - - - - - - - - - - - - Tests that the response of the coauth lock metadata is empty if the metadata header is empty. - - - - - - - - - - - - - - - - - - - - - - - - - Tests that the response of the coauth lock metadata is empty if the metadata header is omitted. - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a refresh coauth lock request with new metadata passed changes the metadata for the coauth lock in the coauth table. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a refresh coauth lock request without metadata passed has no effect on the coauth lock metadata. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests the failing of GetCoauthLock requests when missing the required parameter of CoauthLockType. - - - - - - - - - - - - Tests the failing of GetCoauthLock requests when missing the required parameter of CoauthLockId. - - - - - - - - - - - - Tests the failing of GetCoauthLock requests when missing the required parameter of CoauthLockExpirationTimeout. - - - - - - - - - - - - Tests the failing of UnlockCoauth requests when missing the required parameter of CoauthLockId. - - - - - - - - - - - - Tests the failing of RefreshCoauthLock requests when missing the required parameter of CoauthLockId. - - - - - - - - - - - - Tests the failing of RefreshCoauthLock requests when missing the required parameter of CoauthLockExpirationTimeout. - - - - - - - - - - - - Tests when the coauth lock type is set to "None" instead of "Coauth" or "CoauthExclusive." - - - - - - - - - - - - Tests when the coauth lock type is empty. - - - - - - - - - - - - Tests that the CoauthLockId parameter is outside size limitations of 1024 ASCII characters for GetCoauthLock. - - - - - - - - - - - - Tests that the CoauthLockId parameter is outside size limitations of 1024 ASCII characters for UnlockCoauthLock. - - - - - - - - - - - - Tests that the CoauthLockId parameter is within size limitations of 1024 ASCII characters for RefreshCoauthLock. - - - - - - - - - - - - Tests invalid timeout values for CoauthLockExpirationTimeout for GetCoauthLock. - - - - - - - - - - - - - - - - - Tests invalid timeout values for CoauthLockExpirationTimeout for RefreshCoauthLock. - - - - - - - - - - - - - - - - - - - - - - - Tests when a coauth exclusive lock is requested when a coauth exclusive lock already exists. - - - - - - - - - - - - - - - - - - - - - - - - - Tests when a coauth lock type switch from coauth to coauth exclusive lock is requested on an existing coauth exclusive lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests UnlockCoauthLock conflict when requested with no existing locks. - - - - - - - - - - - - Tests RefreshCoauthLock conflict when requested with no existing locks. - - - - - - - - - - - - Tests that the CoauthLockMetadata parameter is within size limits of 4KB for GetCoauthLock. - - - - - - - - - - - - - - - - - - - - - Tests that the CoauthLockMetadata parameter is within size limits of 4KB for RefreshCoauthLock. - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that ConflictingLockUsername is returned when a Coauth lock is requested after an existing Wopi lock with header LockUserVisible set true - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock's metadata is handled properly when coauthlockmetadata is sent as part of body. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock's metadata is handled properly when coauthlockmetadata is sent as part of body. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock's metadata is handled properly when coauthlockmetadata is sent as part of body and header. Body value is given preference over header - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock's metadata is handled properly when coauthlockmetadata is sent as part of body and header. Body value is given preference over header. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock's metadata is handled properly when coauthlockmetadata is sent as part of body and header. Body value is given preference over header. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock's metadata is handled properly when coauthlockmetadata is sent as part of body and header. Body value is given preference over header and set to empty. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock's metadata is handled properly when coauthlockmetadata is sent as part of body. Body value is set to empty. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock's metadata is handled properly when coauthlockmetadata is sent as part of body. Body value is set to empty. - If present and empty or null, the CoauthLockMetadata value remains unchanged. If present and is set to a non-empty string, CoauthLockMetadata is overwritten with that value. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - SupportsCoauthPrereq - SupportsChunkedFileTransferPrereq - - - - - Host file is not locked and size is larger than 0. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PutChunkedFile using a mismatched coauth lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PutChunkedFile with WOPI Lock and Coauth Lock set at the same time. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Host file is locked by WOPI Lock but client "X-WOPI-Lock" value does not match the lock currently on the file. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - File on host is locked by a WOPI Lock and the client provides a Coauth Lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - File on host is locked by CoauthExclusive lock and client present a CoauthLock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - File on host is locked by CoauthExclusiveLock and client presents a Wopi lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - File on host is locked by CoauthLock, and client presents Wopi Lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Sequence number provided by the WOPI client does not match the latest value on the host. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - SupportsCoauthPrereq - SupportsChunkedFileTransferPrereq - - - - - Initiate a single put chunked file request with Coauth lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initiate a single put chunked file request with Coauth Exclusive lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initiate a single put chunked file request with WOPI lock. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Multiple put chunked file requests. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Host file is not locked and size is zero. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Client download streams that are not existed on host should return empty stream. - - - - - - - - - - - - - - - - - - - - - - - Client upload subset of streams of a document, unmentioned streams should remain as they are. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PutChunkedFile request first followed with PutFile request. ContentProperties with Retention = DeleteOnContentChange should be cleaned up. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Client sends GetChunkedFile request with ChunksToReturn set to LastZipChunk. If the file is not a zip archive, this would behave the same as None. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - SupportsCoauthPrereq - SupportsChunkedFileTransferPrereq - - - - - Initiate a single Zip format upload request with Coauth lock, followed by Zip format download request. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initiate a single Zip format upload request with Coauth lock, followed by ChunksToReturn.None Zip format download request. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initiate a single Zip format upload request with Coauth lock, followed by ChunksToReturn.LastZipChunk Zip format download request. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initiate a single FullFile format upload request with Coauth lock, followed by Zip format download, but host returns FullFile format because host content not eligible for Zip. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initiate a single Zip format upload request with Coauth lock, followed by FullFile download request but host returns Zip Chunking format because host content eligible for Zip. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initiate a single Zip format upload request with Coauth Exclusive lock, followed by Zip format download. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Initiate a single Zip format upload request with WOPI lock, followed by Zip format download. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Multiple Zip format upload requests, followed by Zip format full download. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Multiple Zip format upload requests, followed by Zip format delta download. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Multiple Zip format upload requests, followed by Zip format delta download. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Host file is not locked and size is zero. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Client download streams that are not existed on host should return empty stream. - - - - - - - - - - - - - - - - - - - - - - - Client upload subset of streams of a document, unmentioned streams should remain as they are. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PutChunkedFile request first followed with PutFile request. ContentProperties with Retention = DeleteOnContentChange should be cleaned up. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WopiValidatorPrereq - FileEditingPrereq - LocksPrereq - RenameFilePrereq - WopiValidatorPrereq - SupportsCoauthPrereq - - - - - Client sends RenameFile request with same CoauthLock on host. Request should succeed with file content unchanged. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Client sends RenameFile request with CoauthLock and host file is unlocked. Request should succeed. - - - - - - - - - - - - - - - - - - - - - - Client sends RenameFile request with neither Wopi nor CoauthLock being set and host file is unlocked. Request should succeed. - - - - - - - - - - - - - - - - - - - - - - Client sends RenameFile request with neither Wopi nor CoauthLock being set. Host file is locked by CoauthLock. Request should fail with 409 Conflict. - - - - - - - - - - - - - - - - - - - - - - - - Client sends RenameFile request with Wopi lock. Host file is locked by CoauthLock. Request should fail with 409 Conflict. - - - - - - - - - - - - - - - - - - - - - - - - Client sends RenameFile request with CoauthLock. Host file is locked by CoauthLock but presented lock id does not exist in coauth table. Request should fail with 409 Conflict. - - - - - - - - - - - - - - - - - - - - - - - - Client sends RenameFile request with CoauthLock. Host file is locked by WopiLock. Request should fail with 409 Conflict. - - - - - - - - - - - - - - - - - - - - - - - - Client sends RenameFile request with WopiLock and CoauthLock being set together. Request should fail with 400 BadRequest. - - - - - - - - - - - - - - - WopiValidatorPrereq - SupportsCoauthPrereq - - - - - Tests that the coauth table request will return only coauth locks that have not expired. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a refresh coauth lock request refreshes the coauth lock expiration timeout. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tests that a coauth lock timeout expiration is reflected in the coauth table. - - - - - - - - - - - - - - - - - - - - - - - - - Tests UnlockCoauthLock on an expired coauth lock. - - - - - - - - - - - - - - - - - - - Tests RefreshCoauthLock on an expired lock. - - - - - - - - - - - - - - - - - - - + UnixTimestampProperties = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "AccessTokenExpiry", + "ServerTime" + }; public JsonSchemaValidator(string schemaId) { @@ -49,25 +58,115 @@ public ValidationResult Validate(IResponseData data, IResourceManager resourceMa private ValidationResult ValidateJsonContent(string jsonContent) { var errors = _schema.Validate(jsonContent); - if (errors.Count == 0) + List errorMessages = new List(); + + // Perform standard JSON schema validation + if (errors.Count > 0) { - return new ValidationResult(); + var grouped = errors.GroupBy(error => error.Kind); + + foreach (var group in grouped) + { + var errorKind = ValidationErrorKindString(group.Key); + var properties = group.Select(error => error.Property); + string propertiesString = String.Join(", ", properties); + errorMessages.Add($"{errorKind}: {propertiesString}"); + } } - List errorMessages = new List(); - var grouped = errors.GroupBy(error => error.Kind); + // Perform additional timestamp validation for Unix timestamp properties + try + { + JObject jObject = JObject.Parse(jsonContent); + var timestampErrors = ValidateTimestampProperties(jObject); + errorMessages.AddRange(timestampErrors); + } + catch (Exception ex) + { + errorMessages.Add($"Error parsing JSON for timestamp validation: {ex.Message}"); + } - foreach (var group in grouped) + if (errorMessages.Count == 0) { - var errorKind = ValidationErrorKindString(group.Key); - var properties = group.Select(error => error.Property); - string propertiesString = String.Join(", ", properties); - errorMessages.Add($"{errorKind}: {propertiesString}"); + return new ValidationResult(); } return new ValidationResult(errorMessages.ToArray()); } + private List ValidateTimestampProperties(JObject jObject) + { + List errors = new List(); + DateTime currentTime = DateTime.UtcNow; + DateTime minValidTime = currentTime.AddYears(-10); + DateTime maxValidTime = currentTime.AddYears(10); + + foreach (var property in UnixTimestampProperties) + { + JToken token = jObject[property]; + if (token != null && token.Type == JTokenType.Integer) + { + try + { + long unixTimestamp = token.Value(); + + // Convert Unix timestamp (seconds since epoch) to DateTime + DateTime dateTime; + try + { + dateTime = UnixEpoch.AddSeconds(unixTimestamp); + } + catch (ArgumentOutOfRangeException) + { + errors.Add($"Property '{property}' has invalid Unix timestamp value '{unixTimestamp}' that cannot be converted to a valid DateTime."); + continue; + } + + // Check if timestamp is within reasonable bounds (current time ± 10 years) + if (dateTime < minValidTime || dateTime > maxValidTime) + { + errors.Add($"Property '{property}' has timestamp value '{unixTimestamp}' ('{dateTime:yyyy-MM-dd HH:mm:ss} UTC') that is outside the valid range of {minValidTime:yyyy-MM-dd HH:mm:ss} UTC to {maxValidTime:yyyy-MM-dd HH:mm:ss} UTC. Timestamps should be within 10 years of current time."); + } + } + catch (Exception ex) + { + errors.Add($"Property '{property}' timestamp validation failed: {ex.Message}"); + } + } + else if (token != null && token.Type == JTokenType.Float) + { + try + { + double unixTimestamp = token.Value(); + + // Convert Unix timestamp (seconds since epoch) to DateTime + DateTime dateTime; + try + { + dateTime = UnixEpoch.AddSeconds(unixTimestamp); + } + catch (ArgumentOutOfRangeException) + { + errors.Add($"Property '{property}' has invalid Unix timestamp value '{unixTimestamp}' that cannot be converted to a valid DateTime."); + continue; + } + + // Check if timestamp is within reasonable bounds (current time ± 10 years) + if (dateTime < minValidTime || dateTime > maxValidTime) + { + errors.Add($"Property '{property}' has timestamp value '{unixTimestamp}' ('{dateTime:yyyy-MM-dd HH:mm:ss} UTC') that is outside the valid range of {minValidTime:yyyy-MM-dd HH:mm:ss} UTC to {maxValidTime:yyyy-MM-dd HH:mm:ss} UTC. Timestamps should be within 10 years of current time."); + } + } + catch (Exception ex) + { + errors.Add($"Property '{property}' timestamp validation failed: {ex.Message}"); + } + } + } + + return errors; + } + private string ValidationErrorKindString(JsonValidation.ValidationErrorKind kind) { switch (kind) diff --git a/test/WopiValidator.Core.Tests/Validators/JsonSchemaValidatorUnitTests.cs b/test/WopiValidator.Core.Tests/Validators/JsonSchemaValidatorUnitTests.cs index 0fcb087..76409ba 100644 --- a/test/WopiValidator.Core.Tests/Validators/JsonSchemaValidatorUnitTests.cs +++ b/test/WopiValidator.Core.Tests/Validators/JsonSchemaValidatorUnitTests.cs @@ -4,12 +4,22 @@ using Microsoft.Office.WopiValidator.Core; using Microsoft.Office.WopiValidator.Core.Validators; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; namespace Microsoft.Office.WopiValidator.UnitTests.Validators { [TestClass] public class JsonSchemaValidatorUnitTests { + // Unix timestamp helper for .NET Framework 4.5 compatibility + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + private static long ToUnixTimeSeconds(DateTime dateTime) + { + return (long)(dateTime.ToUniversalTime() - UnixEpoch).TotalSeconds; + } + [TestMethod] public void Validate_CheckFileInfoSchema_Succeeds() { @@ -115,5 +125,250 @@ public void Validate_CheckFileInfoSchema_DefinedOptionalFields_Succeeds() ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); Assert.IsFalse(result.HasFailures); } + + [TestMethod] + public void Validate_AccessTokenExpiry_ValidTimestamp_Succeeds() + { + // Valid timestamp: one hour from now + long validTimestamp = ToUnixTimeSeconds(DateTime.UtcNow.AddHours(1)); + + string jsonInput = $"{{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"," + + $"\"AccessTokenExpiry\": {validTimestamp}}}"; + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsFalse(result.HasFailures); + } + + [TestMethod] + public void Validate_AccessTokenExpiry_OldTimestamp_Fails() + { + // Invalid timestamp: 15 years ago + long invalidTimestamp = ToUnixTimeSeconds(DateTime.UtcNow.AddYears(-15)); + + string jsonInput = $"{{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"," + + $"\"AccessTokenExpiry\": {invalidTimestamp}}}"; + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsTrue(result.HasFailures); + Assert.IsTrue(result.Errors.Any(e => e.Contains("AccessTokenExpiry"))); + Assert.IsTrue(result.Errors.Any(e => e.Contains("outside the valid range"))); + } + + [TestMethod] + public void Validate_AccessTokenExpiry_FutureTimestamp_Fails() + { + // Invalid timestamp: 15 years in the future + long invalidTimestamp = ToUnixTimeSeconds(DateTime.UtcNow.AddYears(15)); + + string jsonInput = $"{{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"," + + $"\"AccessTokenExpiry\": {invalidTimestamp}}}"; + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsTrue(result.HasFailures); + Assert.IsTrue(result.Errors.Any(e => e.Contains("AccessTokenExpiry"))); + Assert.IsTrue(result.Errors.Any(e => e.Contains("outside the valid range"))); + } + + [TestMethod] + public void Validate_ServerTime_ValidTimestamp_Succeeds() + { + // Valid timestamp: current time + long validTimestamp = ToUnixTimeSeconds(DateTime.UtcNow); + + string jsonInput = $"{{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"," + + $"\"ServerTime\": {validTimestamp}}}"; + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsFalse(result.HasFailures); + } + + [TestMethod] + public void Validate_ServerTime_InvalidTimestamp_Fails() + { + // Invalid timestamp: 15 years ago + long invalidTimestamp = ToUnixTimeSeconds(DateTime.UtcNow.AddYears(-15)); + + string jsonInput = $"{{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"," + + $"\"ServerTime\": {invalidTimestamp}}}"; + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsTrue(result.HasFailures); + Assert.IsTrue(result.Errors.Any(e => e.Contains("ServerTime"))); + Assert.IsTrue(result.Errors.Any(e => e.Contains("outside the valid range"))); + } + + [TestMethod] + public void Validate_FileExtension_ValidFormat_Succeeds() + { + string jsonInput = + "{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"," + + "\"FileExtension\": \".docx\"}"; + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsFalse(result.HasFailures); + } + + [TestMethod] + public void Validate_FileExtension_InvalidFormat_Fails() + { + string jsonInput = + "{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"," + + "\"FileExtension\": \"docx\"}"; // Missing dot + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsTrue(result.HasFailures); + } + + [TestMethod] + public void Validate_SHA256_ValidFormat_Succeeds() + { + string jsonInput = + "{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"," + + "\"SHA256\": \"a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890\"}"; + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsFalse(result.HasFailures); + } + + [TestMethod] + public void Validate_SHA256_InvalidFormat_Fails() + { + string jsonInput = + "{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"," + + "\"SHA256\": \"invalid-sha256\"}"; // Invalid format + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsTrue(result.HasFailures); + } + + [TestMethod] + public void Validate_EmptyRequiredFields_Fails() + { + string jsonInput = + "{\"BaseFileName\": \"\"," + // Empty required field + "\"OwnerId\": \"owner123\"," + + "\"Size\": 1024," + + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"}"; + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsTrue(result.HasFailures); + } + + [TestMethod] + public void Validate_NegativeSize_Fails() + { + string jsonInput = + "{\"BaseFileName\": \"test.docx\"," + + "\"OwnerId\": \"owner123\"," + + "\"Size\": -100," + // Negative size + "\"UserId\": \"user123\"," + + "\"Version\": \"1.0\"}"; + + IResponseData response = new ResponseDataMock + { + IsTextResponse = true, + ResponseContentText = jsonInput, + }; + + ValidationResult result = new JsonSchemaValidator("CheckFileInfoSchema").Validate(response, null, null); + Assert.IsTrue(result.HasFailures); + } } }