Skip to content

Commit 18a4edf

Browse files
authored
Merge pull request #1568 from json-api-dotnet/openapi-update-swashbuckle
Test against the latest prerelease version of Swashbuckle
2 parents e35da84 + 3c48e2c commit 18a4edf

File tree

10 files changed

+68
-61
lines changed

10 files changed

+68
-61
lines changed

.gitattributes

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# On Windows, these text files are auto-converted to crlf on git fetch, while the written downloaded files use lf line endings.
33
# Therefore, running the tests on Windows creates local changes. Staging them auto-converts back to crlf, which undoes the changes.
44
# To avoid this annoyance, the next line opts out of the auto-conversion and forces line endings to lf.
5-
**/GeneratedSwagger/*.json text eol=lf
5+
**/GeneratedSwagger/**/*.json text eol=lf

nuget.config

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<configuration>
3+
<packageSources>
4+
<add key="Swashbuckle MyGet" value="https://www.myget.org/F/domaindrivendev/api/v3/index.json" />
5+
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
6+
</packageSources>
7+
</configuration>

package-versions.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<NSwagApiClientVersion>13.20.*</NSwagApiClientVersion>
2424
<NewtonsoftJsonVersion>13.0.*</NewtonsoftJsonVersion>
2525
<SourceLinkVersion>8.0.*</SourceLinkVersion>
26-
<SwashbuckleVersion>6.6.*</SwashbuckleVersion>
26+
<SwashbuckleVersion>6.*-*</SwashbuckleVersion>
2727
<TestSdkVersion>17.10.*</TestSdkVersion>
2828
<XunitVersion>2.8.*</XunitVersion>
2929
</PropertyGroup>

test/OpenApiKiotaEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@ public async Task Cannot_exceed_min_length_constraint()
232232
Attributes = new SocialMediaAccountAttributesInPostRequest
233233
{
234234
LastName = newAccount.LastName,
235-
Password = "YQ=="
235+
// Using -3 instead of -1 to compensate for base64 padding.
236+
Password = Convert.ToBase64String(Enumerable.Repeat((byte)'X', SocialMediaAccount.MinPasswordChars - 3).ToArray())
236237
}
237238
}
238239
};
@@ -244,9 +245,11 @@ public async Task Cannot_exceed_min_length_constraint()
244245
ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync<ErrorResponseDocument>()).Which;
245246
document.Errors.ShouldHaveCount(1);
246247

248+
const int minCharsInBase64 = SocialMediaAccount.MinPasswordCharsInBase64;
249+
247250
ErrorObject errorObject = document.Errors.First();
248251
errorObject.Title.Should().Be("Input validation failed.");
249-
errorObject.Detail.Should().Be("The field Password must be a string or array type with a minimum length of '5'.");
252+
errorObject.Detail.Should().Be($"The field Password must be a string or array type with a minimum length of '{minCharsInBase64}'.");
250253
errorObject.Source.ShouldNotBeNull();
251254
errorObject.Source.Pointer.Should().Be("/data/attributes/password");
252255
}
@@ -268,7 +271,7 @@ public async Task Cannot_exceed_max_length_constraint()
268271
Attributes = new SocialMediaAccountAttributesInPostRequest
269272
{
270273
LastName = newAccount.LastName,
271-
Password = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=="
274+
Password = Convert.ToBase64String(Enumerable.Repeat((byte)'X', SocialMediaAccount.MaxPasswordChars + 1).ToArray())
272275
}
273276
}
274277
};
@@ -280,9 +283,11 @@ public async Task Cannot_exceed_max_length_constraint()
280283
ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync<ErrorResponseDocument>()).Which;
281284
document.Errors.ShouldHaveCount(1);
282285

286+
const int maxCharsInBase64 = SocialMediaAccount.MaxPasswordCharsInBase64;
287+
283288
ErrorObject errorObject = document.Errors.First();
284289
errorObject.Title.Should().Be("Input validation failed.");
285-
errorObject.Detail.Should().Be("The field Password must be a string or array type with a maximum length of '100'.");
290+
errorObject.Detail.Should().Be($"The field Password must be a string or array type with a maximum length of '{maxCharsInBase64}'.");
286291
errorObject.Source.ShouldNotBeNull();
287292
errorObject.Source.Pointer.Should().Be("/data/attributes/password");
288293
}
@@ -304,7 +309,7 @@ public async Task Cannot_use_invalid_base64()
304309
Attributes = new SocialMediaAccountAttributesInPostRequest
305310
{
306311
LastName = newAccount.LastName,
307-
Password = "not_base_64"
312+
Password = "not-a-valid-base64-string"
308313
}
309314
}
310315
};
@@ -380,7 +385,7 @@ public async Task Cannot_use_relative_url()
380385
Attributes = new SocialMediaAccountAttributesInPostRequest
381386
{
382387
LastName = newAccount.LastName,
383-
BackgroundPicture = "relativeurl"
388+
BackgroundPicture = "relative-url"
384389
}
385390
}
386391
};

test/OpenApiNSwagEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ public async Task Cannot_exceed_min_length_constraint()
226226
Attributes = new SocialMediaAccountAttributesInPostRequest
227227
{
228228
LastName = newAccount.LastName,
229-
Password = "YQ=="
229+
// Using -3 instead of -1 to compensate for base64 padding.
230+
Password = Enumerable.Repeat((byte)'X', SocialMediaAccount.MinPasswordChars - 3).ToArray()
230231
}
231232
}
232233
};
@@ -238,9 +239,11 @@ public async Task Cannot_exceed_min_length_constraint()
238239
ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync<ApiException<ErrorResponseDocument>>()).Which.Result;
239240
document.Errors.ShouldHaveCount(1);
240241

242+
const int minCharsInBase64 = SocialMediaAccount.MinPasswordCharsInBase64;
243+
241244
ErrorObject errorObject = document.Errors.First();
242245
errorObject.Title.Should().Be("Input validation failed.");
243-
errorObject.Detail.Should().Be("The field Password must be a string or array type with a minimum length of '5'.");
246+
errorObject.Detail.Should().Be($"The field Password must be a string or array type with a minimum length of '{minCharsInBase64}'.");
244247
errorObject.Source.ShouldNotBeNull();
245248
errorObject.Source.Pointer.Should().Be("/data/attributes/password");
246249
}
@@ -262,7 +265,7 @@ public async Task Cannot_exceed_max_length_constraint()
262265
Attributes = new SocialMediaAccountAttributesInPostRequest
263266
{
264267
LastName = newAccount.LastName,
265-
Password = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=="
268+
Password = Enumerable.Repeat((byte)'X', SocialMediaAccount.MaxPasswordChars + 1).ToArray()
266269
}
267270
}
268271
};
@@ -274,45 +277,11 @@ public async Task Cannot_exceed_max_length_constraint()
274277
ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync<ApiException<ErrorResponseDocument>>()).Which.Result;
275278
document.Errors.ShouldHaveCount(1);
276279

277-
ErrorObject errorObject = document.Errors.First();
278-
errorObject.Title.Should().Be("Input validation failed.");
279-
errorObject.Detail.Should().Be("The field Password must be a string or array type with a maximum length of '100'.");
280-
errorObject.Source.ShouldNotBeNull();
281-
errorObject.Source.Pointer.Should().Be("/data/attributes/password");
282-
}
283-
284-
[Fact]
285-
public async Task Cannot_use_invalid_base64()
286-
{
287-
// Arrange
288-
SocialMediaAccount newAccount = _fakers.SocialMediaAccount.Generate();
289-
290-
using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler);
291-
ModelStateValidationClient apiClient = new(httpClient);
292-
293-
SocialMediaAccountPostRequestDocument requestBody = new()
294-
{
295-
Data = new SocialMediaAccountDataInPostRequest
296-
{
297-
Type = SocialMediaAccountResourceType.SocialMediaAccounts,
298-
Attributes = new SocialMediaAccountAttributesInPostRequest
299-
{
300-
LastName = newAccount.LastName,
301-
Password = "not_base_64"
302-
}
303-
}
304-
};
305-
306-
// Act
307-
Func<Task> action = () => apiClient.PostSocialMediaAccountAsync(requestBody);
308-
309-
// Assert
310-
ErrorResponseDocument document = (await action.Should().ThrowExactlyAsync<ApiException<ErrorResponseDocument>>()).Which.Result;
311-
document.Errors.ShouldHaveCount(1);
280+
const int maxCharsInBase64 = SocialMediaAccount.MaxPasswordCharsInBase64;
312281

313282
ErrorObject errorObject = document.Errors.First();
314283
errorObject.Title.Should().Be("Input validation failed.");
315-
errorObject.Detail.Should().Be("The Password field is not a valid Base64 encoding.");
284+
errorObject.Detail.Should().Be($"The field Password must be a string or array type with a maximum length of '{maxCharsInBase64}'.");
316285
errorObject.Source.ShouldNotBeNull();
317286
errorObject.Source.Pointer.Should().Be("/data/attributes/password");
318287
}
@@ -554,7 +523,7 @@ public async Task Can_create_resource_with_valid_properties()
554523
UserName = newAccount.UserName,
555524
CreditCard = newAccount.CreditCard,
556525
Email = newAccount.Email,
557-
Password = newAccount.Password,
526+
Password = Convert.FromBase64String(newAccount.Password!),
558527
Phone = newAccount.Phone,
559528
Age = newAccount.Age,
560529
ProfilePicture = newAccount.ProfilePicture,

test/OpenApiTests/ModelStateValidation/GeneratedSwagger/net8.0/swagger.g.json

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,10 @@
431431
"nullable": true
432432
},
433433
"password": {
434-
"maxLength": 100,
435-
"minLength": 5,
434+
"maxLength": 60,
435+
"minLength": 20,
436436
"type": "string",
437+
"format": "byte",
437438
"nullable": true
438439
},
439440
"phone": {
@@ -443,7 +444,9 @@
443444
},
444445
"age": {
445446
"maximum": 122.9,
447+
"exclusiveMaximum": true,
446448
"minimum": 0.1,
449+
"exclusiveMinimum": true,
447450
"type": "number",
448451
"format": "double",
449452
"nullable": true
@@ -536,9 +539,10 @@
536539
"nullable": true
537540
},
538541
"password": {
539-
"maxLength": 100,
540-
"minLength": 5,
542+
"maxLength": 60,
543+
"minLength": 20,
541544
"type": "string",
545+
"format": "byte",
542546
"nullable": true
543547
},
544548
"phone": {
@@ -548,7 +552,9 @@
548552
},
549553
"age": {
550554
"maximum": 122.9,
555+
"exclusiveMaximum": true,
551556
"minimum": 0.1,
557+
"exclusiveMinimum": true,
552558
"type": "number",
553559
"format": "double",
554560
"nullable": true
@@ -638,9 +644,10 @@
638644
"nullable": true
639645
},
640646
"password": {
641-
"maxLength": 100,
642-
"minLength": 5,
647+
"maxLength": 60,
648+
"minLength": 20,
643649
"type": "string",
650+
"format": "byte",
644651
"nullable": true
645652
},
646653
"phone": {
@@ -650,7 +657,9 @@
650657
},
651658
"age": {
652659
"maximum": 122.9,
660+
"exclusiveMaximum": true,
653661
"minimum": 0.1,
662+
"exclusiveMinimum": true,
654663
"type": "number",
655664
"format": "double",
656665
"nullable": true

test/OpenApiTests/ModelStateValidation/ModelStateValidationFakers.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ public sealed class ModelStateValidationFakers
1818
.RuleFor(socialMediaAccount => socialMediaAccount.UserName, faker => faker.Random.String2(3, 18))
1919
.RuleFor(socialMediaAccount => socialMediaAccount.CreditCard, faker => faker.Finance.CreditCardNumber())
2020
.RuleFor(socialMediaAccount => socialMediaAccount.Email, faker => faker.Person.Email)
21-
.RuleFor(socialMediaAccount => socialMediaAccount.Password, faker => Convert.ToBase64String(faker.Random.Bytes(faker.Random.Number(4, 75))))
21+
.RuleFor(socialMediaAccount => socialMediaAccount.Password, faker =>
22+
{
23+
int byteCount = faker.Random.Number(ModelStateValidation.SocialMediaAccount.MinPasswordChars,
24+
ModelStateValidation.SocialMediaAccount.MaxPasswordChars);
25+
26+
return Convert.ToBase64String(faker.Random.Bytes(byteCount));
27+
})
2228
.RuleFor(socialMediaAccount => socialMediaAccount.Phone, faker => faker.Person.Phone)
2329
.RuleFor(socialMediaAccount => socialMediaAccount.Age, faker => faker.Random.Double(0.1, 122.9))
2430
.RuleFor(socialMediaAccount => socialMediaAccount.ProfilePicture, faker => new Uri(faker.Image.LoremFlickrUrl()))

test/OpenApiTests/ModelStateValidation/ModelStateValidationTests.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,9 @@ public async Task Min_max_length_annotation_on_resource_property_produces_expect
151151
document.Should().ContainPath($"components.schemas.{modelName}.properties.password").With(passwordElement =>
152152
{
153153
#if !NET6_0
154-
passwordElement.Should().HaveProperty("maxLength", 100);
155-
passwordElement.Should().HaveProperty("minLength", 5);
154+
passwordElement.Should().HaveProperty("format", "byte");
155+
passwordElement.Should().HaveProperty("maxLength", SocialMediaAccount.MaxPasswordCharsInBase64);
156+
passwordElement.Should().HaveProperty("minLength", SocialMediaAccount.MinPasswordCharsInBase64);
156157
#endif
157158
passwordElement.Should().HaveProperty("type", "string");
158159
});
@@ -184,9 +185,11 @@ public async Task Range_annotation_on_resource_property_produces_expected_schema
184185
document.Should().ContainPath($"components.schemas.{modelName}.properties.age").With(ageElement =>
185186
{
186187
ageElement.Should().HaveProperty("maximum", 122.9);
187-
ageElement.Should().NotContainPath("exclusiveMaximum");
188188
ageElement.Should().HaveProperty("minimum", 0.1);
189-
ageElement.Should().NotContainPath("exclusiveMinimum");
189+
#if !NET6_0
190+
ageElement.Should().ContainPath("exclusiveMaximum").With(exclusiveElement => exclusiveElement.Should().Be(true));
191+
ageElement.Should().ContainPath("exclusiveMinimum").With(exclusiveElement => exclusiveElement.Should().Be(true));
192+
#endif
190193
ageElement.Should().HaveProperty("type", "number");
191194
ageElement.Should().HaveProperty("format", "double");
192195
});

test/OpenApiTests/ModelStateValidation/SocialMediaAccount.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ namespace OpenApiTests.ModelStateValidation;
1010
[Resource(ControllerNamespace = "OpenApiTests.ModelStateValidation", GenerateControllerEndpoints = JsonApiEndpoints.Post | JsonApiEndpoints.Patch)]
1111
public sealed class SocialMediaAccount : Identifiable<Guid>
1212
{
13+
public const int MinPasswordChars = 15;
14+
public const int MinPasswordCharsInBase64 = (int)(4.0 / 3 * MinPasswordChars);
15+
16+
public const int MaxPasswordChars = 45;
17+
public const int MaxPasswordCharsInBase64 = (int)(4.0 / 3 * MaxPasswordChars);
18+
1319
[Attr]
1420
public Guid? AlternativeId { get; set; }
1521

@@ -39,8 +45,8 @@ public sealed class SocialMediaAccount : Identifiable<Guid>
3945
[Attr]
4046
#if !NET6_0
4147
[Base64String]
42-
[MinLength(5)]
43-
[MaxLength(100)]
48+
[MinLength(MinPasswordCharsInBase64)]
49+
[MaxLength(MaxPasswordCharsInBase64)]
4450
#endif
4551
public string? Password { get; set; }
4652

test/OpenApiTests/OpenApiTests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<PropertyGroup>
33
<TargetFrameworks>net8.0;net6.0</TargetFrameworks>
44
<GenerateDocumentationFile>True</GenerateDocumentationFile>
5+
<OpenApiGenerateDocuments>false</OpenApiGenerateDocuments>
56
<NoWarn>$(NoWarn);1591</NoWarn>
67
</PropertyGroup>
78

@@ -23,5 +24,6 @@
2324
<PackageReference Include="GitHubActionsTestLogger" Version="$(GitHubActionsTestLoggerVersion)" PrivateAssets="All" />
2425
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(AspNetCoreVersion)" />
2526
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
27+
<PackageReference Include="Swashbuckle.AspNetCore" Version="$(SwashbuckleVersion)" />
2628
</ItemGroup>
2729
</Project>

0 commit comments

Comments
 (0)