diff --git a/.gitattributes b/.gitattributes index 2780241e04..0c78db34f3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,4 @@ # On Windows, these text files are auto-converted to crlf on git fetch, while the written downloaded files use lf line endings. # Therefore, running the tests on Windows creates local changes. Staging them auto-converts back to crlf, which undoes the changes. # To avoid this annoyance, the next line opts out of the auto-conversion and forces line endings to lf. -**/GeneratedSwagger/*.json text eol=lf +**/GeneratedSwagger/**/*.json text eol=lf diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000000..16164967c6 --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/package-versions.props b/package-versions.props index 41238fcf3e..5b844b4c96 100644 --- a/package-versions.props +++ b/package-versions.props @@ -23,7 +23,7 @@ 13.20.* 13.0.* 8.0.* - 6.6.* + 6.*-* 17.10.* 2.8.* diff --git a/test/OpenApiKiotaEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs b/test/OpenApiKiotaEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs index 3f7069c1fb..7727a1b272 100644 --- a/test/OpenApiKiotaEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs +++ b/test/OpenApiKiotaEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs @@ -232,7 +232,8 @@ public async Task Cannot_exceed_min_length_constraint() Attributes = new SocialMediaAccountAttributesInPostRequest { LastName = newAccount.LastName, - Password = "YQ==" + // Using -3 instead of -1 to compensate for base64 padding. + Password = Convert.ToBase64String(Enumerable.Repeat((byte)'X', SocialMediaAccount.MinPasswordChars - 3).ToArray()) } } }; @@ -244,9 +245,11 @@ public async Task Cannot_exceed_min_length_constraint() ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync()).Which; document.Errors.ShouldHaveCount(1); + const int minCharsInBase64 = SocialMediaAccount.MinPasswordCharsInBase64; + ErrorObject errorObject = document.Errors.First(); errorObject.Title.Should().Be("Input validation failed."); - errorObject.Detail.Should().Be("The field Password must be a string or array type with a minimum length of '5'."); + errorObject.Detail.Should().Be($"The field Password must be a string or array type with a minimum length of '{minCharsInBase64}'."); errorObject.Source.ShouldNotBeNull(); errorObject.Source.Pointer.Should().Be("/data/attributes/password"); } @@ -268,7 +271,7 @@ public async Task Cannot_exceed_max_length_constraint() Attributes = new SocialMediaAccountAttributesInPostRequest { LastName = newAccount.LastName, - Password = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==" + Password = Convert.ToBase64String(Enumerable.Repeat((byte)'X', SocialMediaAccount.MaxPasswordChars + 1).ToArray()) } } }; @@ -280,9 +283,11 @@ public async Task Cannot_exceed_max_length_constraint() ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync()).Which; document.Errors.ShouldHaveCount(1); + const int maxCharsInBase64 = SocialMediaAccount.MaxPasswordCharsInBase64; + ErrorObject errorObject = document.Errors.First(); errorObject.Title.Should().Be("Input validation failed."); - errorObject.Detail.Should().Be("The field Password must be a string or array type with a maximum length of '100'."); + errorObject.Detail.Should().Be($"The field Password must be a string or array type with a maximum length of '{maxCharsInBase64}'."); errorObject.Source.ShouldNotBeNull(); errorObject.Source.Pointer.Should().Be("/data/attributes/password"); } @@ -304,7 +309,7 @@ public async Task Cannot_use_invalid_base64() Attributes = new SocialMediaAccountAttributesInPostRequest { LastName = newAccount.LastName, - Password = "not_base_64" + Password = "not-a-valid-base64-string" } } }; @@ -380,7 +385,7 @@ public async Task Cannot_use_relative_url() Attributes = new SocialMediaAccountAttributesInPostRequest { LastName = newAccount.LastName, - BackgroundPicture = "relativeurl" + BackgroundPicture = "relative-url" } } }; diff --git a/test/OpenApiNSwagEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs b/test/OpenApiNSwagEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs index 879cc833cd..7f91408a67 100644 --- a/test/OpenApiNSwagEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs +++ b/test/OpenApiNSwagEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs @@ -226,7 +226,8 @@ public async Task Cannot_exceed_min_length_constraint() Attributes = new SocialMediaAccountAttributesInPostRequest { LastName = newAccount.LastName, - Password = "YQ==" + // Using -3 instead of -1 to compensate for base64 padding. + Password = Enumerable.Repeat((byte)'X', SocialMediaAccount.MinPasswordChars - 3).ToArray() } } }; @@ -238,9 +239,11 @@ public async Task Cannot_exceed_min_length_constraint() ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync>()).Which.Result; document.Errors.ShouldHaveCount(1); + const int minCharsInBase64 = SocialMediaAccount.MinPasswordCharsInBase64; + ErrorObject errorObject = document.Errors.First(); errorObject.Title.Should().Be("Input validation failed."); - errorObject.Detail.Should().Be("The field Password must be a string or array type with a minimum length of '5'."); + errorObject.Detail.Should().Be($"The field Password must be a string or array type with a minimum length of '{minCharsInBase64}'."); errorObject.Source.ShouldNotBeNull(); errorObject.Source.Pointer.Should().Be("/data/attributes/password"); } @@ -262,7 +265,7 @@ public async Task Cannot_exceed_max_length_constraint() Attributes = new SocialMediaAccountAttributesInPostRequest { LastName = newAccount.LastName, - Password = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==" + Password = Enumerable.Repeat((byte)'X', SocialMediaAccount.MaxPasswordChars + 1).ToArray() } } }; @@ -274,45 +277,11 @@ public async Task Cannot_exceed_max_length_constraint() ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync>()).Which.Result; document.Errors.ShouldHaveCount(1); - ErrorObject errorObject = document.Errors.First(); - errorObject.Title.Should().Be("Input validation failed."); - errorObject.Detail.Should().Be("The field Password must be a string or array type with a maximum length of '100'."); - errorObject.Source.ShouldNotBeNull(); - errorObject.Source.Pointer.Should().Be("/data/attributes/password"); - } - - [Fact] - public async Task Cannot_use_invalid_base64() - { - // Arrange - SocialMediaAccount newAccount = _fakers.SocialMediaAccount.Generate(); - - using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); - ModelStateValidationClient apiClient = new(httpClient); - - SocialMediaAccountPostRequestDocument requestBody = new() - { - Data = new SocialMediaAccountDataInPostRequest - { - Type = SocialMediaAccountResourceType.SocialMediaAccounts, - Attributes = new SocialMediaAccountAttributesInPostRequest - { - LastName = newAccount.LastName, - Password = "not_base_64" - } - } - }; - - // Act - Func action = () => apiClient.PostSocialMediaAccountAsync(requestBody); - - // Assert - ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync>()).Which.Result; - document.Errors.ShouldHaveCount(1); + const int maxCharsInBase64 = SocialMediaAccount.MaxPasswordCharsInBase64; ErrorObject errorObject = document.Errors.First(); errorObject.Title.Should().Be("Input validation failed."); - errorObject.Detail.Should().Be("The Password field is not a valid Base64 encoding."); + errorObject.Detail.Should().Be($"The field Password must be a string or array type with a maximum length of '{maxCharsInBase64}'."); errorObject.Source.ShouldNotBeNull(); errorObject.Source.Pointer.Should().Be("/data/attributes/password"); } @@ -554,7 +523,7 @@ public async Task Can_create_resource_with_valid_properties() UserName = newAccount.UserName, CreditCard = newAccount.CreditCard, Email = newAccount.Email, - Password = newAccount.Password, + Password = Convert.FromBase64String(newAccount.Password!), Phone = newAccount.Phone, Age = newAccount.Age, ProfilePicture = newAccount.ProfilePicture, diff --git a/test/OpenApiTests/ModelStateValidation/GeneratedSwagger/net8.0/swagger.g.json b/test/OpenApiTests/ModelStateValidation/GeneratedSwagger/net8.0/swagger.g.json index eaf1c6dd5c..234b2b7e9b 100644 --- a/test/OpenApiTests/ModelStateValidation/GeneratedSwagger/net8.0/swagger.g.json +++ b/test/OpenApiTests/ModelStateValidation/GeneratedSwagger/net8.0/swagger.g.json @@ -431,9 +431,10 @@ "nullable": true }, "password": { - "maxLength": 100, - "minLength": 5, + "maxLength": 60, + "minLength": 20, "type": "string", + "format": "byte", "nullable": true }, "phone": { @@ -443,7 +444,9 @@ }, "age": { "maximum": 122.9, + "exclusiveMaximum": true, "minimum": 0.1, + "exclusiveMinimum": true, "type": "number", "format": "double", "nullable": true @@ -536,9 +539,10 @@ "nullable": true }, "password": { - "maxLength": 100, - "minLength": 5, + "maxLength": 60, + "minLength": 20, "type": "string", + "format": "byte", "nullable": true }, "phone": { @@ -548,7 +552,9 @@ }, "age": { "maximum": 122.9, + "exclusiveMaximum": true, "minimum": 0.1, + "exclusiveMinimum": true, "type": "number", "format": "double", "nullable": true @@ -638,9 +644,10 @@ "nullable": true }, "password": { - "maxLength": 100, - "minLength": 5, + "maxLength": 60, + "minLength": 20, "type": "string", + "format": "byte", "nullable": true }, "phone": { @@ -650,7 +657,9 @@ }, "age": { "maximum": 122.9, + "exclusiveMaximum": true, "minimum": 0.1, + "exclusiveMinimum": true, "type": "number", "format": "double", "nullable": true diff --git a/test/OpenApiTests/ModelStateValidation/ModelStateValidationFakers.cs b/test/OpenApiTests/ModelStateValidation/ModelStateValidationFakers.cs index 0427b52532..1fdc2203da 100644 --- a/test/OpenApiTests/ModelStateValidation/ModelStateValidationFakers.cs +++ b/test/OpenApiTests/ModelStateValidation/ModelStateValidationFakers.cs @@ -18,7 +18,13 @@ public sealed class ModelStateValidationFakers .RuleFor(socialMediaAccount => socialMediaAccount.UserName, faker => faker.Random.String2(3, 18)) .RuleFor(socialMediaAccount => socialMediaAccount.CreditCard, faker => faker.Finance.CreditCardNumber()) .RuleFor(socialMediaAccount => socialMediaAccount.Email, faker => faker.Person.Email) - .RuleFor(socialMediaAccount => socialMediaAccount.Password, faker => Convert.ToBase64String(faker.Random.Bytes(faker.Random.Number(4, 75)))) + .RuleFor(socialMediaAccount => socialMediaAccount.Password, faker => + { + int byteCount = faker.Random.Number(ModelStateValidation.SocialMediaAccount.MinPasswordChars, + ModelStateValidation.SocialMediaAccount.MaxPasswordChars); + + return Convert.ToBase64String(faker.Random.Bytes(byteCount)); + }) .RuleFor(socialMediaAccount => socialMediaAccount.Phone, faker => faker.Person.Phone) .RuleFor(socialMediaAccount => socialMediaAccount.Age, faker => faker.Random.Double(0.1, 122.9)) .RuleFor(socialMediaAccount => socialMediaAccount.ProfilePicture, faker => new Uri(faker.Image.LoremFlickrUrl())) diff --git a/test/OpenApiTests/ModelStateValidation/ModelStateValidationTests.cs b/test/OpenApiTests/ModelStateValidation/ModelStateValidationTests.cs index e4d6ef04c2..a27c2e4d25 100644 --- a/test/OpenApiTests/ModelStateValidation/ModelStateValidationTests.cs +++ b/test/OpenApiTests/ModelStateValidation/ModelStateValidationTests.cs @@ -151,8 +151,9 @@ public async Task Min_max_length_annotation_on_resource_property_produces_expect document.Should().ContainPath($"components.schemas.{modelName}.properties.password").With(passwordElement => { #if !NET6_0 - passwordElement.Should().HaveProperty("maxLength", 100); - passwordElement.Should().HaveProperty("minLength", 5); + passwordElement.Should().HaveProperty("format", "byte"); + passwordElement.Should().HaveProperty("maxLength", SocialMediaAccount.MaxPasswordCharsInBase64); + passwordElement.Should().HaveProperty("minLength", SocialMediaAccount.MinPasswordCharsInBase64); #endif passwordElement.Should().HaveProperty("type", "string"); }); @@ -184,9 +185,11 @@ public async Task Range_annotation_on_resource_property_produces_expected_schema document.Should().ContainPath($"components.schemas.{modelName}.properties.age").With(ageElement => { ageElement.Should().HaveProperty("maximum", 122.9); - ageElement.Should().NotContainPath("exclusiveMaximum"); ageElement.Should().HaveProperty("minimum", 0.1); - ageElement.Should().NotContainPath("exclusiveMinimum"); +#if !NET6_0 + ageElement.Should().ContainPath("exclusiveMaximum").With(exclusiveElement => exclusiveElement.Should().Be(true)); + ageElement.Should().ContainPath("exclusiveMinimum").With(exclusiveElement => exclusiveElement.Should().Be(true)); +#endif ageElement.Should().HaveProperty("type", "number"); ageElement.Should().HaveProperty("format", "double"); }); diff --git a/test/OpenApiTests/ModelStateValidation/SocialMediaAccount.cs b/test/OpenApiTests/ModelStateValidation/SocialMediaAccount.cs index 1938618e0f..5f6c8fcc2f 100644 --- a/test/OpenApiTests/ModelStateValidation/SocialMediaAccount.cs +++ b/test/OpenApiTests/ModelStateValidation/SocialMediaAccount.cs @@ -10,6 +10,12 @@ namespace OpenApiTests.ModelStateValidation; [Resource(ControllerNamespace = "OpenApiTests.ModelStateValidation", GenerateControllerEndpoints = JsonApiEndpoints.Post | JsonApiEndpoints.Patch)] public sealed class SocialMediaAccount : Identifiable { + public const int MinPasswordChars = 15; + public const int MinPasswordCharsInBase64 = (int)(4.0 / 3 * MinPasswordChars); + + public const int MaxPasswordChars = 45; + public const int MaxPasswordCharsInBase64 = (int)(4.0 / 3 * MaxPasswordChars); + [Attr] public Guid? AlternativeId { get; set; } @@ -39,8 +45,8 @@ public sealed class SocialMediaAccount : Identifiable [Attr] #if !NET6_0 [Base64String] - [MinLength(5)] - [MaxLength(100)] + [MinLength(MinPasswordCharsInBase64)] + [MaxLength(MaxPasswordCharsInBase64)] #endif public string? Password { get; set; } diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index 111a345368..09797c657e 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -2,6 +2,7 @@ net8.0;net6.0 True + false $(NoWarn);1591 @@ -23,5 +24,6 @@ +