|
4 | 4 | package types |
5 | 5 |
|
6 | 6 | import ( |
| 7 | + "encoding/json" |
| 8 | + "maps" |
| 9 | + "reflect" |
| 10 | + "slices" |
7 | 11 | "testing" |
8 | 12 |
|
9 | 13 | "github.com/stretchr/testify/require" |
@@ -78,11 +82,90 @@ func TestExtractIDs(t *testing.T) { |
78 | 82 | for _, tt := range tests { |
79 | 83 | t.Run(tt.name, func(t *testing.T) { |
80 | 84 | intf := AzureInterface{} |
81 | | - intf.SetID(tt.resourceID) |
| 85 | + intf.ID = tt.resourceID |
82 | 86 |
|
83 | 87 | require.Equal(t, tt.expectedRG, intf.GetResourceGroup()) |
84 | 88 | require.Equal(t, tt.expectedVMID, intf.GetVMID()) |
85 | 89 | require.Equal(t, tt.expectedVMSSName, intf.GetVMScaleSetName()) |
86 | 90 | }) |
87 | 91 | } |
88 | 92 | } |
| 93 | + |
| 94 | +// State the status carries but does not serialize cannot survive the apiserver, |
| 95 | +// so the operator's freshly built status would never compare equal to the copy |
| 96 | +// it reads back and every IPAM sync would write /status. |
| 97 | +func TestAzureStatusHasNoUnserializedState(t *testing.T) { |
| 98 | + status := reflect.TypeFor[AzureStatus]() |
| 99 | + |
| 100 | + var check func(ty reflect.Type) |
| 101 | + check = func(ty reflect.Type) { |
| 102 | + // Only the types declared here are walked. |
| 103 | + for ty.Kind() == reflect.Pointer || ty.Kind() == reflect.Slice || ty.Kind() == reflect.Array || ty.Kind() == reflect.Map { |
| 104 | + ty = ty.Elem() |
| 105 | + } |
| 106 | + if ty.Kind() != reflect.Struct || ty.PkgPath() != status.PkgPath() { |
| 107 | + return |
| 108 | + } |
| 109 | + for i := range ty.NumField() { |
| 110 | + field := ty.Field(i) |
| 111 | + require.True(t, field.IsExported(), "%s.%s is unexported", ty.Name(), field.Name) |
| 112 | + require.NotEqual(t, "-", field.Tag.Get("json"), "%s.%s is excluded from JSON", ty.Name(), field.Name) |
| 113 | + check(field.Type) |
| 114 | + } |
| 115 | + } |
| 116 | + check(status) |
| 117 | +} |
| 118 | + |
| 119 | +// A round trip must not perturb the interface, or the operator's DeepEqual |
| 120 | +// write-skip gate breaks. |
| 121 | +func TestAzureInterfaceJSONRoundTrip(t *testing.T) { |
| 122 | + base := &AzureInterface{ |
| 123 | + ID: "/subscriptions/xxx/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines/0/networkInterfaces/vmss1", |
| 124 | + Name: "eth0", |
| 125 | + MAC: "aa:bb:cc:dd:ee:ff", |
| 126 | + State: StateSucceeded, |
| 127 | + SecurityGroup: "sg1", |
| 128 | + Addresses: []AzureAddress{ |
| 129 | + {IP: "10.0.0.2", Subnet: "s-1", State: StateSucceeded}, |
| 130 | + {IP: "10.0.0.3", Subnet: "s-1", State: StateSucceeded}, |
| 131 | + }, |
| 132 | + GatewayIP: "10.0.0.1", |
| 133 | + Gateway: "10.0.0.1", |
| 134 | + CIDR: "10.0.0.0/24", |
| 135 | + } |
| 136 | + |
| 137 | + marshalled, err := json.Marshal(base) |
| 138 | + require.NoError(t, err) |
| 139 | + var roundTripped AzureInterface |
| 140 | + require.NoError(t, json.Unmarshal(marshalled, &roundTripped)) |
| 141 | + |
| 142 | + require.Equal(t, *base, roundTripped) |
| 143 | + require.True(t, base.DeepEqual(&roundTripped)) |
| 144 | + |
| 145 | + // A difference in any field, including the nested types, must be visible to |
| 146 | + // DeepEqual or the operator skips a /status write it owes. |
| 147 | + mutations := map[string]func(*AzureInterface){ |
| 148 | + "ID": func(a *AzureInterface) { a.ID = "intf-2" }, |
| 149 | + "Name": func(a *AzureInterface) { a.Name = "eth1" }, |
| 150 | + "MAC": func(a *AzureInterface) { a.MAC = "ff:ee:dd:cc:bb:aa" }, |
| 151 | + "State": func(a *AzureInterface) { a.State = "failed" }, |
| 152 | + "SecurityGroup": func(a *AzureInterface) { a.SecurityGroup = "sg2" }, |
| 153 | + "Addresses[].IP": func(a *AzureInterface) { a.Addresses[0].IP = "10.0.0.9" }, |
| 154 | + "Addresses[].Subnet": func(a *AzureInterface) { a.Addresses[0].Subnet = "s-9" }, |
| 155 | + "Addresses[].State": func(a *AzureInterface) { a.Addresses[0].State = "failed" }, |
| 156 | + "GatewayIP": func(a *AzureInterface) { a.GatewayIP = "10.0.1.1" }, |
| 157 | + "Gateway": func(a *AzureInterface) { a.Gateway = "10.0.1.1" }, |
| 158 | + "CIDR": func(a *AzureInterface) { a.CIDR = "10.0.1.0/24" }, |
| 159 | + |
| 160 | + "Addresses removed": func(a *AzureInterface) { a.Addresses = nil }, |
| 161 | + "Addresses reordered": func(a *AzureInterface) { a.Addresses[0], a.Addresses[1] = a.Addresses[1], a.Addresses[0] }, |
| 162 | + } |
| 163 | + |
| 164 | + for _, path := range slices.Sorted(maps.Keys(mutations)) { |
| 165 | + t.Run(path, func(t *testing.T) { |
| 166 | + other := roundTripped.DeepCopy() |
| 167 | + mutations[path](other) |
| 168 | + require.False(t, base.DeepEqual(other)) |
| 169 | + }) |
| 170 | + } |
| 171 | +} |
0 commit comments