From 1fa6dfb14e8286dcb065a2e6d53abc33162de7a7 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Sun, 23 Jun 2024 08:22:16 +0200
Subject: [PATCH 1/2] Run tests against the latest prerelease version of
Swashbuckle, which supports new .NET 8 ModelState attributes
---
nuget.config | 7 +++
package-versions.props | 2 +-
.../ModelStateValidationTests.cs | 17 ++++---
.../ModelStateValidationTests.cs | 49 ++++---------------
.../GeneratedSwagger/net8.0/swagger.g.json | 21 +++++---
.../ModelStateValidationFakers.cs | 8 ++-
.../ModelStateValidationTests.cs | 11 +++--
.../SocialMediaAccount.cs | 10 +++-
test/OpenApiTests/OpenApiTests.csproj | 2 +
9 files changed, 67 insertions(+), 60 deletions(-)
create mode 100644 nuget.config
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 @@
+
From 3c48e2c92e30ae55e0f81e3bf371a79c32f6b21b Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Sun, 23 Jun 2024 08:41:21 +0200
Subject: [PATCH 2/2] Account for swagger.g.json per target framework
---
.gitattributes | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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