@@ -146,6 +146,102 @@ struct TestCIDR {
146146 #expect( !cidr. contains ( . v6( ip) ) )
147147 }
148148
149+ // MARK: - Wrapper Agrees With The Concrete Types
150+
151+ // `CIDR` keeps the address exactly as parsed, so a block written from a
152+ // host address ("192.168.1.100/24") stores a value with host bits set.
153+ // Containment has to mask both sides, the way `CIDRv4`/`CIDRv6` do, or the
154+ // block reports that it excludes its own members.
155+ @Test ( arguments: [
156+ ( " 192.168.1.100/24 " , " 192.168.1.100 " ) ,
157+ ( " 192.168.1.100/24 " , " 192.168.1.0 " ) ,
158+ ( " 192.168.1.100/24 " , " 192.168.1.255 " ) ,
159+ ( " 10.1.2.3/16 " , " 10.1.99.99 " ) ,
160+ ( " 2001:db8::1234/64 " , " 2001:db8::1 " ) ,
161+ ( " 2001:db8::1234/64 " , " 2001:db8::1234 " ) ,
162+ ] )
163+ func testWrapperContainsMembersOfAHostAddressBlock( cidr: String , ip: String ) throws {
164+ let block = try CIDR ( cidr)
165+ #expect( block. contains ( try IPAddress ( ip) ) )
166+ }
167+
168+ @Test ( arguments: [
169+ ( " 192.168.1.100/24 " , " 192.168.2.1 " ) ,
170+ ( " 10.1.2.3/16 " , " 10.2.0.1 " ) ,
171+ ( " 2001:db8::1234/64 " , " 2001:db9::1 " ) ,
172+ ] )
173+ func testWrapperStillExcludesNonMembers( cidr: String , ip: String ) throws {
174+ let block = try CIDR ( cidr)
175+ #expect( !block. contains ( try IPAddress ( ip) ) )
176+ }
177+
178+ // The wrapper must never disagree with the type it wraps.
179+ @Test ( arguments: [ " 192.168.1.100/24 " , " 10.1.2.3/16 " , " 192.168.1.0/24 " , " 1.2.3.4/32 " ] )
180+ func testWrapperMatchesCIDRv4( cidr: String ) throws {
181+ let wrapper = try CIDR ( cidr)
182+ let concrete = try CIDRv4 ( cidr)
183+ for probe in [ " 192.168.1.100 " , " 192.168.1.0 " , " 10.1.99.99 " , " 1.2.3.4 " , " 8.8.8.8 " ] {
184+ let ip = try IPv4Address ( probe)
185+ #expect( wrapper. contains ( . v4( ip) ) == concrete. contains ( ip) , " disagreed on \( probe) for \( cidr) " )
186+ }
187+ #expect( wrapper. lower. description == concrete. lower. description)
188+ #expect( wrapper. upper. description == concrete. upper. description)
189+ }
190+
191+ @Test ( arguments: [ " 2001:db8::1234/64 " , " fe80::1%eth0/64 " , " 2001:db8::/32 " ] )
192+ func testWrapperMatchesCIDRv6( cidr: String ) throws {
193+ let wrapper = try CIDR ( cidr)
194+ let concrete = try CIDRv6 ( cidr)
195+ for probe in [ " 2001:db8::1 " , " 2001:db8::1234 " , " fe80::1%eth0 " , " fe80::1 " , " 2001:db9::1 " ] {
196+ let ip = try IPv6Address ( probe)
197+ #expect( wrapper. contains ( . v6( ip) ) == concrete. contains ( ip) , " disagreed on \( probe) for \( cidr) " )
198+ }
199+ #expect( wrapper. lower. description == concrete. lower. description)
200+ #expect( wrapper. upper. description == concrete. upper. description)
201+ }
202+
203+ // MARK: - Zone Preservation in Bounds
204+
205+ // `contains` compares zones, so a bound that drops the zone is reported as
206+ // outside its own block. Both bounds have to carry the address's zone for
207+ // the block to contain them.
208+ @Test ( arguments: [ " fe80::1%eth0/64 " , " fe80::1%eth0/128 " , " 2001:db8::5%lo0/126 " ] )
209+ func testZonedBlockContainsItsOwnBounds( cidr: String ) throws {
210+ let block = try CIDRv6 ( cidr)
211+ #expect( block. contains ( block. lower) )
212+ #expect( block. contains ( block. upper) )
213+ }
214+
215+ @Test ( arguments: [ " fe80::1%eth0/64 " , " 2001:db8::5%lo0/126 " ] )
216+ func testBoundsAgreeOnZone( cidr: String ) throws {
217+ let block = try CIDRv6 ( cidr)
218+ #expect( block. lower. zone == block. address. zone)
219+ #expect( block. upper. zone == block. address. zone)
220+ }
221+
222+ @Test func testZonedLowerRendersItsZone( ) throws {
223+ let block = try CIDRv6 ( " 2001:db8::5%lo0/126 " )
224+ #expect( block. lower. description == " 2001:db8::4%lo0 " )
225+ #expect( block. upper. description == " 2001:db8::7%lo0 " )
226+ }
227+
228+ // The same bounds live on the `CIDR` wrapper, which carries its own copy.
229+ @Test func testZonedWrapperContainsItsOwnBounds( ) throws {
230+ let block = try CIDR ( " fe80::1%eth0/64 " )
231+ #expect( block. contains ( block. lower) )
232+ #expect( block. contains ( block. upper) )
233+ #expect( block. lower. description == " fe80::%eth0 " )
234+ }
235+
236+ // An unzoned block must keep rendering without a zone suffix.
237+ @Test func testUnzonedBoundsCarryNoZone( ) throws {
238+ let block = try CIDRv6 ( " 2001:db8::5/126 " )
239+ #expect( block. lower. zone == nil )
240+ #expect( block. upper. zone == nil )
241+ #expect( block. lower. description == " 2001:db8::4 " )
242+ #expect( try CIDR ( " 2001:db8::5/126 " ) . lower. description == " 2001:db8::4 " )
243+ }
244+
149245 // MARK: - Range Constructor
150246
151247 @Test func testRangeConstructorFindsSmallestBlock( ) throws {
0 commit comments