|
8 | 8 | using System.Text.Json; |
9 | 9 | using System.Text.Json.Nodes; |
10 | 10 | using System.Text.Json.Serialization; |
| 11 | +using Azure.Bicep.Types; |
11 | 12 | using Azure.Bicep.Types.Concrete; |
| 13 | +using Azure.Bicep.Types.Index; |
12 | 14 | using Azure.Bicep.Types.Serialization; |
13 | 15 | using FluentAssertions; |
14 | 16 | using Microsoft.VisualStudio.TestTools.UnitTesting; |
@@ -414,6 +416,157 @@ public void ResourceType_with_legacy_unknown_scope_deserializes_as_all() |
414 | 416 | deserializedResource.WritableScopes.Should().Be(ScopeType.All); |
415 | 417 | } |
416 | 418 |
|
| 419 | + [TestMethod] |
| 420 | + public void ResourceType_with_modern_scopes_should_not_serialize_null_legacy_properties() |
| 421 | + { |
| 422 | + var factory = new TypeFactory(Enumerable.Empty<TypeBase>()); |
| 423 | + var stringType = factory.Create(() => new StringType()); |
| 424 | + var objectType = factory.Create(() => new ObjectType( |
| 425 | + name: "request@v1", |
| 426 | + properties: new Dictionary<string, ObjectTypeProperty> |
| 427 | + { |
| 428 | + ["uri"] = new(factory.GetReference(stringType), ObjectTypePropertyFlags.Required, "The HTTP request URI") |
| 429 | + }, |
| 430 | + additionalProperties: null)); |
| 431 | + |
| 432 | + // Create ResourceType with ONLY modern properties |
| 433 | + var resourceType = factory.Create(() => new ResourceType( |
| 434 | + name: "request@v1", |
| 435 | + body: factory.GetReference(objectType), |
| 436 | + functions: null, |
| 437 | + writableScopes_in: ScopeType.All, |
| 438 | + readableScopes_in: ScopeType.All)); |
| 439 | + |
| 440 | + // Serialize |
| 441 | + string serializedJson; |
| 442 | + using (var stream = new MemoryStream()) |
| 443 | + { |
| 444 | + TypeSerializer.Serialize(stream, factory.GetTypes()); |
| 445 | + serializedJson = Encoding.UTF8.GetString(stream.ToArray()); |
| 446 | + } |
| 447 | + |
| 448 | + var doc = JsonDocument.Parse(serializedJson); |
| 449 | + var resourceElement = doc.RootElement.EnumerateArray() |
| 450 | + .First(e => e.TryGetProperty("$type", out var typeVal) && typeVal.GetString() == "ResourceType"); |
| 451 | + |
| 452 | + // Check for legacy properties |
| 453 | + bool hasScopeType = resourceElement.TryGetProperty("scopeType", out _); |
| 454 | + bool hasReadOnlyScopes = resourceElement.TryGetProperty("readOnlyScopes", out _); |
| 455 | + bool hasFlags = resourceElement.TryGetProperty("flags", out _); |
| 456 | + |
| 457 | + // Verify modern properties are present |
| 458 | + bool hasReadableScopes = resourceElement.TryGetProperty("readableScopes", out var readableVal); |
| 459 | + bool hasWritableScopes = resourceElement.TryGetProperty("writableScopes", out var writableVal); |
| 460 | + |
| 461 | + // Assert: Modern properties should be present |
| 462 | + Assert.IsTrue(hasReadableScopes, "readableScopes should be present in JSON"); |
| 463 | + Assert.IsTrue(hasWritableScopes, "writableScopes should be present in JSON"); |
| 464 | + Assert.AreEqual(31, readableVal.GetInt32(), "readableScopes should be 31 (All)"); |
| 465 | + Assert.AreEqual(31, writableVal.GetInt32(), "writableScopes should be 31 (All)"); |
| 466 | + |
| 467 | + // Assert: Legacy properties should NOT be present (they are null and should be omitted) |
| 468 | + Assert.IsFalse(hasScopeType, |
| 469 | + "scopeType should not be serialized when null (JsonIgnoreCondition.WhenWritingNull)"); |
| 470 | + Assert.IsFalse(hasReadOnlyScopes, |
| 471 | + "readOnlyScopes should not be serialized when null (JsonIgnoreCondition.WhenWritingNull)"); |
| 472 | + Assert.IsFalse(hasFlags, |
| 473 | + "flags should not be serialized when null (JsonIgnoreCondition.WhenWritingNull)"); |
| 474 | + } |
| 475 | + |
| 476 | + [TestMethod] |
| 477 | + public void ResourceType_with_legacy_scopes_should_serialize_and_deserialize_correctly() |
| 478 | + { |
| 479 | + var factory = new TypeFactory(Enumerable.Empty<TypeBase>()); |
| 480 | + var stringType = factory.Create(() => new StringType()); |
| 481 | + var objectType = factory.Create(() => new ObjectType( |
| 482 | + name: "legacy@v1", |
| 483 | + properties: new Dictionary<string, ObjectTypeProperty> |
| 484 | + { |
| 485 | + ["id"] = new(factory.GetReference(stringType), ObjectTypePropertyFlags.Required, "Resource ID") |
| 486 | + }, |
| 487 | + additionalProperties: null)); |
| 488 | + |
| 489 | + // Create with legacy properties |
| 490 | + var resourceType = factory.Create(() => new ResourceType( |
| 491 | + name: "legacy@v1", |
| 492 | + body: factory.GetReference(objectType), |
| 493 | + functions: null, |
| 494 | + scopeType: ScopeType.ResourceGroup, |
| 495 | + readOnlyScopes: null, |
| 496 | + flags: ResourceFlags.None)); |
| 497 | + |
| 498 | + // Serialize |
| 499 | + string serializedJson; |
| 500 | + using (var stream = new MemoryStream()) |
| 501 | + { |
| 502 | + TypeSerializer.Serialize(stream, factory.GetTypes()); |
| 503 | + serializedJson = Encoding.UTF8.GetString(stream.ToArray()); |
| 504 | + } |
| 505 | + |
| 506 | + var doc = JsonDocument.Parse(serializedJson); |
| 507 | + var resourceElement = doc.RootElement.EnumerateArray() |
| 508 | + .First(e => e.TryGetProperty("$type", out var typeVal) && typeVal.GetString() == "ResourceType"); |
| 509 | + |
| 510 | + // Deserialize |
| 511 | + TypeBase[] deserializedTypes; |
| 512 | + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(serializedJson))) |
| 513 | + { |
| 514 | + deserializedTypes = TypeSerializer.Deserialize(stream); |
| 515 | + } |
| 516 | + |
| 517 | + var deserialized = deserializedTypes.OfType<ResourceType>().First(); |
| 518 | + |
| 519 | + Assert.AreEqual(ScopeType.ResourceGroup, deserialized.ReadableScopes); |
| 520 | + Assert.AreEqual(ScopeType.ResourceGroup, deserialized.WritableScopes); |
| 521 | + } |
| 522 | + |
| 523 | + [TestMethod] |
| 524 | + public void ResourceType_modern_scopes_roundtrip_preserves_values() |
| 525 | + { |
| 526 | + var factory = new TypeFactory(Enumerable.Empty<TypeBase>()); |
| 527 | + var stringType = factory.Create(() => new StringType()); |
| 528 | + var objectType = factory.Create(() => new ObjectType( |
| 529 | + name: "roundtrip@v1", |
| 530 | + properties: new Dictionary<string, ObjectTypeProperty> |
| 531 | + { |
| 532 | + ["name"] = new(factory.GetReference(stringType), ObjectTypePropertyFlags.Required, "Name") |
| 533 | + }, |
| 534 | + additionalProperties: null)); |
| 535 | + |
| 536 | + // Create with modern properties |
| 537 | + var original = factory.Create(() => new ResourceType( |
| 538 | + name: "roundtrip@v1", |
| 539 | + body: factory.GetReference(objectType), |
| 540 | + functions: null, |
| 541 | + writableScopes_in: ScopeType.Subscription | ScopeType.ResourceGroup, |
| 542 | + readableScopes_in: ScopeType.All)); |
| 543 | + |
| 544 | + // Serialize |
| 545 | + string serializedJson; |
| 546 | + using (var stream = new MemoryStream()) |
| 547 | + { |
| 548 | + TypeSerializer.Serialize(stream, factory.GetTypes()); |
| 549 | + serializedJson = Encoding.UTF8.GetString(stream.ToArray()); |
| 550 | + } |
| 551 | + |
| 552 | + // Deserialize |
| 553 | + TypeBase[] deserializedTypes; |
| 554 | + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(serializedJson))) |
| 555 | + { |
| 556 | + deserializedTypes = TypeSerializer.Deserialize(stream); |
| 557 | + } |
| 558 | + |
| 559 | + var deserialized = deserializedTypes.OfType<ResourceType>().First(); |
| 560 | + |
| 561 | + // Verify scopes match exactly |
| 562 | + Assert.AreEqual(original.ReadableScopes, deserialized.ReadableScopes, |
| 563 | + "ReadableScopes should be preserved through roundtrip"); |
| 564 | + Assert.AreEqual(original.WritableScopes, deserialized.WritableScopes, |
| 565 | + "WritableScopes should be preserved through roundtrip"); |
| 566 | + Assert.AreEqual(ScopeType.All, deserialized.ReadableScopes); |
| 567 | + Assert.AreEqual(ScopeType.Subscription | ScopeType.ResourceGroup, deserialized.WritableScopes); |
| 568 | + } |
| 569 | + |
417 | 570 | private static Stream BuildStream(Action<Stream> writeFunc) |
418 | 571 | { |
419 | 572 | var memoryStream = new MemoryStream(); |
|
0 commit comments