* loosen date-time validation

* recode, fix test error, update snapshot

* update numberOfSpecs
This commit is contained in:
KeYu(AnkhSpirit) 2022-01-26 11:34:07 +08:00 коммит произвёл GitHub
Родитель 7ad54bc872
Коммит eb0d22d652
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 725 добавлений и 30 удалений

Просмотреть файл

@ -2,6 +2,7 @@
## 01/21/2022 2.11.5
- LiveValidator - Loosen date-time validation for missing "Z" in the end
- LiveValidator - Validate multipleOf data again when ajv validation results have error about multipleOf in response
## 12/15/2021 2.11.4

Просмотреть файл

@ -229,6 +229,21 @@ const shouldSkipError = (error: ErrorObject, cxt: SchemaValidateContext) => {
return true;
}
// If payload has property with date-time parameter and its value is valid except missing "Z" in the end we can skip this error
if (keyword === "format" && schema === "date-time" && typeof data === "string") {
const reg = /^\d+-(0\d|1[0-2])-([02]\d|3[01])T([01]\d|2[0-3]):[0-5][0-9]:[0-5][0-9]/;
// intercept time, example: 2008-09-22T14:01:54
const time = data.slice(0, 19);
if (reg.test(time)) {
const dateZ = new Date(data + "Z").toUTCString();
// validate hour
const ifHoursAreSame = time.slice(11, 13) === dateZ.slice(17, 19);
// validate day for leap year, example: 2008-02-29
const ifDaysAreSame = time.slice(8, 10) === dateZ.slice(5, 7);
return ifHoursAreSame && ifDaysAreSame;
}
}
// If a response data has multipleOf property, and it divided by multipleOf value is an integer, we can skip this error
if (keyword === "multipleOf" && typeof schema === "number" && typeof data === "number") {
let [newSchema, newData] = [schema, data];

Просмотреть файл

@ -766,34 +766,8 @@ Object {
"runtimeException": undefined,
},
"responseValidationResult": Object {
"errors": Array [
Object {
"code": "INVALID_FORMAT",
"documentationUrl": "",
"jsonPathsInPayload": Array [
"$.properties.createdAtUtc",
],
"message": "Object didn't pass validation for format date-time: 0001-01-01T00:00:00",
"params": Array [
"date-time",
"0001-01-01T00:00:00",
],
"pathsInPayload": Array [
"/properties/createdAtUtc",
],
"schemaPath": "#/properties/createdAtUtc/format",
"severity": 0,
"source": Object {
"jsonRef": "#/definitions/ApiManagementServiceBaseProperties/properties/createdAtUtc",
"position": Object {
"column": 25,
"line": 903,
},
"url": "specification/apimanagement/resource-manager/Microsoft.ApiManagement/preview/2018-01-01/apimdeployment.json",
},
},
],
"isSuccessful": false,
"errors": Array [],
"isSuccessful": true,
"operationInfo": Object {
"apiVersion": "2018-01-01",
"operationId": "ApiManagementService_Get",

Просмотреть файл

@ -0,0 +1,94 @@
{
"liveRequest": {
"headers": {
"Content-Type": "application/json",
"expect": "aaaaaaaaaaaa",
"host": "management.azure.com",
"user-Agent": "AzurePowershell/v0.0.0;PSVersion/v5.1.17763.2268;Az.DesktopVirtualization/3.1.0",
"x-ms-unique-id": "a",
"x-ms-client-request-id": "6314700c-9e77-4fb7-b62a-ad8d24aef6fc",
"commandName": "Az.DesktopVirtualization\\New-AzWvdHostPool",
"fullCommandName": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"parameterSetName": "__AllParameterSets",
"x-ms-arm-resource-system-data": "a"
},
"method": "PUT",
"url": "/subscriptions/be5ce746-52ca-483a-9c9b-782111a94978/resourceGroups/rgpwvd/providers/Microsoft.DesktopVirtualization/hostPools/svpwvdhp?api-version=2021-07-12",
"body": {
"location": "aaaaaaaaaa",
"properties": {
"registrationInfo": {
"expirationTime": "2022-01-21T02:33:35.0000000",
"registrationTokenOperation": "Update"
},
"friendlyName": "",
"description": "",
"hostPoolType": "Pooled",
"customRdpProperty": "a",
"maxSessionLimit": 4,
"loadBalancerType": "BreadthFirst",
"ring": 0,
"validationEnvironment": false,
"vmTemplate": "",
"preferredAppGroupType": "Desktop"
}
}
},
"liveResponse": {
"statusCode": "OK",
"headers": {
"Content-Type": "application/json",
"transfer-Encoding": "aaaaaaa",
"vary": "Accept-Encoding",
"x-Rate-Limit-Limit": "a",
"x-Rate-Limit-Remaining": "aaaaaaaaaaaaaaaaaaa",
"x-Rate-Limit-Reset": "a",
"x-ms-opsarmpath64": "a",
"x-ms-lamport-ts": "aaaaaaaaaa",
"x-ms-correlation-id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"x-ms-request-id": "6314700c-9e77-4fb7-b62a-ad8d24aef6fc",
"x-Content-Type-Options": "aaaaaaa",
"set-Cookie": "a",
"server": "Microsoft-IIS/10.0",
"x-Powered-By": "aaaaaaa",
"date": "Thu, 20 Jan 2022 22:33:36 GMT"
},
"body": {
"name": "aaaaaaaa",
"id": "a",
"type": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"location": "aaaaaaaaaa",
"tags": null,
"kind": null,
"properties": {
"friendlyName": null,
"description": null,
"hostPoolType": "Pooled",
"personalDesktopAssignmentType": null,
"applicationGroupReferences": [
"a"
],
"customRdpProperty": "a",
"maxSessionLimit": 4,
"loadBalancerType": "BreadthFirst",
"validationEnvironment": false,
"ring": null,
"registrationInfo": {
"expirationTime": "2024-02-29T02:33:35",
"token": "a",
"registrationTokenOperation": "Update"
},
"vmTemplate": null,
"preferredAppGroupType": "Desktop",
"migrationRequest": null,
"cloudPcResource": false,
"startVMOnConnect": false,
"ssoadfsAuthority": null,
"ssoClientId": null,
"ssoClientSecretKeyVaultPath": null,
"ssoSecretType": null,
"objectId": "a"
}
}
}
}

Просмотреть файл

@ -0,0 +1,592 @@
{
"swagger": "2.0",
"info": {
"version": "2021-07-12",
"title": "Desktop Virtualization API Client"
},
"schemes": [
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"host": "management.azure.com",
"securityDefinitions": {
"azure_auth": {
"type": "oauth2",
"authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize",
"flow": "implicit",
"description": "Azure Active Directory OAuth2 Flow",
"scopes": {
"user_impersonation": "impersonate your user account"
}
}
},
"paths": {
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DesktopVirtualization/hostPools/{hostPoolName}": {
"put": {
"tags": [
"HostPool"
],
"operationId": "HostPools_CreateOrUpdate",
"description": "Create or update a host pool.",
"parameters": [
{
"$ref": "#/parameters/ApiVersionParameter"
},
{
"$ref": "#/parameters/SubscriptionIdParameter"
},
{
"$ref": "#/parameters/ResourceGroupNameParameter"
},
{
"$ref": "#/parameters/HostPoolNameParameter"
},
{
"name": "hostPool",
"description": "Object containing HostPool definitions.",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/HostPool"
}
}
],
"responses": {
"200": {
"description": "Successfully updated host pool.",
"schema": {
"$ref": "#/definitions/HostPool"
}
},
"201": {
"description": "Successfully created host pool.",
"schema": {
"$ref": "#/definitions/HostPool"
}
},
"default": {
"description": "Automation error response describing why the operation failed.",
"schema": {
"$ref": "#/definitions/CloudError"
}
}
}
}
}
},
"definitions": {
"CloudError": {
"x-ms-external": true,
"properties": {
"error": {
"$ref": "#/definitions/CloudErrorProperties"
}
}
},
"CloudErrorProperties": {
"type": "object",
"properties": {
"code": {
"description": "Error code",
"type": "string"
},
"message": {
"description": "Error message indicating why the operation failed.",
"type": "string"
}
}
},
"HostPool": {
"type": "object",
"description": "Represents a HostPool definition.",
"allOf": [
{
"$ref": "#/definitions/ResourceModelWithAllowedPropertySet"
}
],
"required": [
"properties"
],
"properties": {
"properties": {
"description": "Detailed properties for HostPool",
"x-ms-client-flatten": true,
"$ref": "#/definitions/HostPoolProperties",
"x-nullable": false
}
}
},
"ResourceModelWithAllowedPropertySet": {
"description": "The resource model definition containing the full set of allowed properties for a resource. Except properties bag, there cannot be a top level property outside of this set.",
"type": "object",
"properties": {
"id": {
"readOnly": true,
"type": "string",
"x-ms-mutability": [
"read"
],
"description": "Fully qualified resource ID for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}"
},
"name": {
"readOnly": true,
"type": "string",
"description": "The name of the resource"
},
"type": {
"readOnly": true,
"type": "string",
"x-ms-mutability": [
"read"
],
"description": "The type of the resource. E.g. \"Microsoft.Compute/virtualMachines\" or \"Microsoft.Storage/storageAccounts\""
},
"location": {
"type": "string",
"x-ms-mutability": [
"read",
"create"
],
"description": "The geo-location where the resource lives"
},
"managedBy": {
"type": "string",
"x-ms-mutability": [
"read",
"create",
"update"
],
"description": "The fully qualified resource ID of the resource that manages this resource. Indicates if this resource is managed by another Azure resource. If this is present, complete mode deployment will not delete the resource if it is removed from the template since it is managed by another resource."
},
"kind": {
"type": "string",
"x-ms-mutability": [
"read",
"create"
],
"description": "Metadata used by portal/tooling/etc to render different UX experiences for resources of the same type; e.g. ApiApps are a kind of Microsoft.Web/sites type. If supported, the resource provider must validate and persist this value.",
"pattern": "^[-\\w\\._,\\(\\)]+$"
},
"etag": {
"readOnly": true,
"type": "string",
"description": "The etag field is *not* required. If it is provided in the response body, it must also be provided as a header per the normal etag convention. Entity tags are used for comparing two or more entities from the same requested resource. HTTP/1.1 uses entity tags in the etag (section 14.19), If-Match (section 14.24), If-None-Match (section 14.26), and If-Range (section 14.27) header fields. "
},
"tags": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"x-ms-mutability": [
"read",
"create",
"update"
],
"description": "Resource tags."
},
"identity": {
"allOf": [
{
"$ref": "#/definitions/Identity"
}
]
},
"sku": {
"allOf": [
{
"$ref": "#/definitions/Sku"
}
]
},
"plan": {
"allOf": [
{
"$ref": "#/definitions/Plan"
}
]
}
},
"x-ms-azure-resource": true
},
"Sku": {
"description": "The resource model definition representing SKU",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the SKU. Ex - P3. It is typically a letter+number code"
},
"tier": {
"type": "string",
"enum": [
"Free",
"Basic",
"Standard",
"Premium"
],
"x-ms-enum": {
"name": "SkuTier",
"modelAsString": false
},
"description": "This field is required to be implemented by the Resource Provider if the service has more than one tier, but is not required on a PUT."
},
"size": {
"type": "string",
"description": "The SKU size. When the name field is the combination of tier and some other value, this would be the standalone code. "
},
"family": {
"type": "string",
"description": "If the service has different generations of hardware, for the same SKU, then that can be captured here."
},
"capacity": {
"type": "integer",
"format": "int32",
"description": "If the SKU supports scale out/in then the capacity integer should be included. If scale out/in is not possible for the resource this may be omitted."
}
},
"required": [
"name"
]
},
"Identity": {
"description": "Identity for the resource.",
"type": "object",
"properties": {
"principalId": {
"readOnly": true,
"type": "string",
"description": "The principal ID of resource identity."
},
"tenantId": {
"readOnly": true,
"type": "string",
"description": "The tenant ID of resource."
},
"type": {
"type": "string",
"description": "The identity type.",
"enum": [
"SystemAssigned"
],
"x-ms-enum": {
"name": "ResourceIdentityType",
"modelAsString": false
}
}
}
},
"Plan": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A user defined name of the 3rd Party Artifact that is being procured."
},
"publisher": {
"type": "string",
"description": "The publisher of the 3rd Party Artifact that is being bought. E.g. NewRelic"
},
"product": {
"type": "string",
"description": "The 3rd Party artifact that is being procured. E.g. NewRelic. Product maps to the OfferID specified for the artifact at the time of Data Market onboarding. "
},
"promotionCode": {
"type": "string",
"description": "A publisher provided promotion code as provisioned in Data Market for the said product/artifact."
},
"version": {
"type": "string",
"description": "The version of the desired product/artifact."
}
},
"description": "Plan for the resource.",
"required": [
"name",
"publisher",
"product"
]
},
"HostPoolProperties": {
"description": "Properties of HostPool.",
"type": "object",
"required": [
"hostPoolType",
"loadBalancerType",
"preferredAppGroupType"
],
"properties": {
"objectId": {
"type": "string",
"description": "ObjectId of HostPool. (internal use)",
"readOnly": true
},
"friendlyName": {
"type": "string",
"description": "Friendly name of HostPool."
},
"description": {
"type": "string",
"description": "Description of HostPool."
},
"hostPoolType": {
"enum": [
"Personal",
"Pooled",
"BYODesktop"
],
"x-ms-enum": {
"name": "HostPoolType",
"modelAsString": true,
"values": [
{
"value": "Personal",
"description": "Users will be assigned a SessionHost either by administrators (PersonalDesktopAssignmentType = Direct) or upon connecting to the pool (PersonalDesktopAssignmentType = Automatic). They will always be redirected to their assigned SessionHost."
},
{
"value": "Pooled",
"description": "Users get a new (random) SessionHost every time it connects to the HostPool."
},
{
"value": "BYODesktop",
"description": "Users assign their own machines, load balancing logic remains the same as Personal. PersonalDesktopAssignmentType must be Direct."
}
]
},
"type": "string",
"description": "HostPool type for desktop.",
"x-nullable": false
},
"personalDesktopAssignmentType": {
"enum": [
"Automatic",
"Direct"
],
"x-ms-enum": {
"name": "PersonalDesktopAssignmentType",
"modelAsString": true
},
"type": "string",
"description": "PersonalDesktopAssignment type for HostPool."
},
"customRdpProperty": {
"type": "string",
"description": "Custom rdp property of HostPool."
},
"maxSessionLimit": {
"type": "integer",
"description": "The max session limit of HostPool."
},
"loadBalancerType": {
"enum": [
"BreadthFirst",
"DepthFirst",
"Persistent"
],
"x-ms-enum": {
"name": "LoadBalancerType",
"modelAsString": true
},
"type": "string",
"description": "The type of the load balancer.",
"x-nullable": false
},
"ring": {
"type": "integer",
"description": "The ring number of HostPool."
},
"validationEnvironment": {
"type": "boolean",
"description": "Is validation environment."
},
"registrationInfo": {
"$ref": "#/definitions/RegistrationInfo",
"description": "The registration info of HostPool."
},
"vmTemplate": {
"type": "string",
"description": "VM template for sessionhosts configuration within hostpool."
},
"applicationGroupReferences": {
"readOnly": true,
"type": "array",
"description": "List of applicationGroup links.",
"items": {
"type": "string"
}
},
"ssoadfsAuthority": {
"type": "string",
"description": "URL to customer ADFS server for signing WVD SSO certificates."
},
"ssoClientId": {
"type": "string",
"description": "ClientId for the registered Relying Party used to issue WVD SSO certificates."
},
"ssoClientSecretKeyVaultPath": {
"type": "string",
"description": "Path to Azure KeyVault storing the secret used for communication to ADFS."
},
"ssoSecretType": {
"enum": [
"SharedKey",
"Certificate",
"SharedKeyInKeyVault",
"CertificateInKeyVault"
],
"x-ms-enum": {
"name": "SSOSecretType",
"modelAsString": true
},
"type": "string",
"description": "The type of single sign on Secret Type."
},
"preferredAppGroupType": {
"enum": [
"None",
"Desktop",
"RailApplications"
],
"x-ms-enum": {
"name": "PreferredAppGroupType",
"modelAsString": true
},
"type": "string",
"description": "The type of preferred application group type, default to Desktop Application Group",
"x-nullable": false
},
"startVMOnConnect": {
"type": "boolean",
"description": "The flag to turn on/off StartVMOnConnect feature."
},
"migrationRequest": {
"$ref": "#/definitions/MigrationRequestProperties",
"description": "The registration info of HostPool."
},
"cloudPcResource": {
"type": "boolean",
"description": "Is cloud pc resource.",
"readOnly": true
}
}
},
"RegistrationInfo": {
"description": "Represents a RegistrationInfo definition.",
"type": "object",
"properties": {
"expirationTime": {
"format": "date-time",
"type": "string",
"description": "Expiration time of registration token."
},
"token": {
"type": "string",
"description": "The registration token base64 encoded string."
},
"registrationTokenOperation": {
"enum": [
"Delete",
"None",
"Update"
],
"x-ms-enum": {
"name": "RegistrationTokenOperation",
"modelAsString": true
},
"type": "string",
"description": "The type of resetting the token.",
"x-nullable": false
}
}
},
"MigrationRequestProperties": {
"type": "object",
"description": "Properties for arm migration.",
"properties": {
"operation": {
"enum": [
"Start",
"Revoke",
"Complete",
"Hide",
"Unhide"
],
"x-ms-enum": {
"name": "Operation",
"modelAsString": true,
"values": [
{
"value": "Start",
"description": "Start the migration."
},
{
"value": "Revoke",
"description": "Revoke the migration."
},
{
"value": "Complete",
"description": "Complete the migration."
},
{
"value": "Hide",
"description": "Hide the hostpool."
},
{
"value": "Unhide",
"description": "Unhide the hostpool."
}
]
},
"type": "string",
"description": "The type of operation for migration."
},
"migrationPath": {
"type": "string",
"description": "The path to the legacy object to migrate."
}
}
}
},
"parameters": {
"ApiVersionParameter": {
"name": "api-version",
"in": "query",
"required": true,
"type": "string",
"description": "The API version to use for this operation.",
"minLength": 1
},
"SubscriptionIdParameter": {
"name": "subscriptionId",
"in": "path",
"required": true,
"type": "string",
"description": "The ID of the target subscription.",
"minLength": 1
},
"ResourceGroupNameParameter": {
"name": "resourceGroupName",
"in": "path",
"required": true,
"type": "string",
"description": "The name of the resource group. The name is case insensitive.",
"minLength": 1,
"maxLength": 90,
"x-ms-parameter-location": "method"
},
"HostPoolNameParameter": {
"name": "hostPoolName",
"in": "path",
"required": true,
"type": "string",
"description": "The name of the host pool within the specified resource group",
"maxLength": 64,
"minLength": 3,
"x-ms-parameter-location": "method"
}
}
}

Просмотреть файл

@ -13,7 +13,7 @@ import * as Constants from "../lib/util/constants";
// eslint-disable-next-line no-var
var glob = require("glob").glob;
const numberOfSpecs = 16;
const numberOfSpecs = 17;
jest.setTimeout(999999);
describe("Live Validator", () => {
@ -715,7 +715,7 @@ describe("Live Validator", () => {
};
const validator = new LiveValidator(options);
await validator.initialize();
const payload = require(`${__dirname}/liveValidation/payloads/missingResponseHeader_shouldSucceed.json`);
const result = await validator.validateLiveRequestResponse(payload);
assert.strictEqual(result.responseValidationResult.isSuccessful, true);
@ -1030,6 +1030,25 @@ describe("Live Validator", () => {
const errors = result.responseValidationResult.errors;
assert.deepStrictEqual(errors, []);
});
it(`should not report error when payload has property with date-time parameter and its value is valid except missing "Z" in the end`, async () => {
const options = {
directory: `${__dirname}/liveValidation/swaggers/`,
isPathCaseSensitive: false,
useRelativeSourceLocationUrl: true,
swaggerPathsPattern: [
"specification/desktopvirtualization/resource-manager/Microsoft.DesktopVirtualization/*.json",
],
git: {
shouldClone: false,
},
};
const liveValidator = new LiveValidator(options);
await liveValidator.initialize();
const payload = require(`${__dirname}/liveValidation/payloads/dateTime.json`);
const result = await liveValidator.validateLiveRequestResponse(payload);
assert.equal(result.responseValidationResult.isSuccessful, true);
});
});
});
describe("Live validator snapshot validation", () => {