github.com/opentofu/opentofu@v1.7.1/internal/lang/funcs/cidr_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package funcs
     7  
     8  import (
     9  	"fmt"
    10  	"testing"
    11  
    12  	"github.com/zclconf/go-cty/cty"
    13  )
    14  
    15  func TestCidrHost(t *testing.T) {
    16  	tests := []struct {
    17  		Prefix  cty.Value
    18  		Hostnum cty.Value
    19  		Want    cty.Value
    20  		Err     bool
    21  	}{
    22  		{
    23  			cty.StringVal("192.168.1.0/24"),
    24  			cty.NumberIntVal(5),
    25  			cty.StringVal("192.168.1.5"),
    26  			false,
    27  		},
    28  		{
    29  			cty.StringVal("192.168.1.0/24"),
    30  			cty.NumberIntVal(-5),
    31  			cty.StringVal("192.168.1.251"),
    32  			false,
    33  		},
    34  		{
    35  			cty.StringVal("192.168.1.0/24"),
    36  			cty.NumberIntVal(-256),
    37  			cty.StringVal("192.168.1.0"),
    38  			false,
    39  		},
    40  		{
    41  			// We inadvertently inherited a pre-Go1.17 standard library quirk
    42  			// if parsing zero-prefix parts as decimal rather than octal.
    43  			// Go 1.17 resolved that quirk by making zero-prefix invalid, but
    44  			// we've preserved our existing behavior for backward compatibility,
    45  			// on the grounds that these functions are for generating addresses
    46  			// rather than validating or processing them. We do always generate
    47  			// a canonical result regardless of the input, though.
    48  			cty.StringVal("010.001.0.0/24"),
    49  			cty.NumberIntVal(6),
    50  			cty.StringVal("10.1.0.6"),
    51  			false,
    52  		},
    53  		{
    54  			cty.StringVal("192.168.1.0/30"),
    55  			cty.NumberIntVal(255),
    56  			cty.UnknownVal(cty.String),
    57  			true, // 255 doesn't fit in two bits
    58  		},
    59  		{
    60  			cty.StringVal("192.168.1.0/30"),
    61  			cty.NumberIntVal(-255),
    62  			cty.UnknownVal(cty.String),
    63  			true, // 255 doesn't fit in two bits
    64  		},
    65  		{
    66  			cty.StringVal("not-a-cidr"),
    67  			cty.NumberIntVal(6),
    68  			cty.UnknownVal(cty.String),
    69  			true, // not a valid CIDR mask
    70  		},
    71  		{
    72  			cty.StringVal("10.256.0.0/8"),
    73  			cty.NumberIntVal(6),
    74  			cty.UnknownVal(cty.String),
    75  			true, // can't have an octet >255
    76  		},
    77  		{ // fractions are Not Ok
    78  			cty.StringVal("10.256.0.0/8"),
    79  			cty.NumberFloatVal(.75),
    80  			cty.UnknownVal(cty.String),
    81  			true,
    82  		},
    83  	}
    84  
    85  	for _, test := range tests {
    86  		t.Run(fmt.Sprintf("cidrhost(%#v, %#v)", test.Prefix, test.Hostnum), func(t *testing.T) {
    87  			got, err := CidrHost(test.Prefix, test.Hostnum)
    88  
    89  			if test.Err {
    90  				if err == nil {
    91  					t.Fatal("succeeded; want error")
    92  				}
    93  				return
    94  			} else if err != nil {
    95  				t.Fatalf("unexpected error: %s", err)
    96  			}
    97  
    98  			if !got.RawEquals(test.Want) {
    99  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   100  			}
   101  		})
   102  	}
   103  }
   104  
   105  func TestCidrNetmask(t *testing.T) {
   106  	tests := []struct {
   107  		Prefix cty.Value
   108  		Want   cty.Value
   109  		Err    bool
   110  	}{
   111  		{
   112  			cty.StringVal("192.168.1.0/24"),
   113  			cty.StringVal("255.255.255.0"),
   114  			false,
   115  		},
   116  		{
   117  			cty.StringVal("192.168.1.0/32"),
   118  			cty.StringVal("255.255.255.255"),
   119  			false,
   120  		},
   121  		{
   122  			cty.StringVal("0.0.0.0/0"),
   123  			cty.StringVal("0.0.0.0"),
   124  			false,
   125  		},
   126  		{
   127  			// We inadvertently inherited a pre-Go1.17 standard library quirk
   128  			// if parsing zero-prefix parts as decimal rather than octal.
   129  			// Go 1.17 resolved that quirk by making zero-prefix invalid, but
   130  			// we've preserved our existing behavior for backward compatibility,
   131  			// on the grounds that these functions are for generating addresses
   132  			// rather than validating or processing them.
   133  			cty.StringVal("010.001.0.0/24"),
   134  			cty.StringVal("255.255.255.0"),
   135  			false,
   136  		},
   137  		{
   138  			cty.StringVal("not-a-cidr"),
   139  			cty.UnknownVal(cty.String),
   140  			true, // not a valid CIDR mask
   141  		},
   142  		{
   143  			cty.StringVal("110.256.0.0/8"),
   144  			cty.UnknownVal(cty.String),
   145  			true, // can't have an octet >255
   146  		},
   147  		{
   148  			cty.StringVal("1::/64"),
   149  			cty.UnknownVal(cty.String),
   150  			true, // IPv6 is invalid
   151  		},
   152  	}
   153  
   154  	for _, test := range tests {
   155  		t.Run(fmt.Sprintf("cidrnetmask(%#v)", test.Prefix), func(t *testing.T) {
   156  			got, err := CidrNetmask(test.Prefix)
   157  
   158  			if test.Err {
   159  				if err == nil {
   160  					t.Fatal("succeeded; want error")
   161  				}
   162  				return
   163  			} else if err != nil {
   164  				t.Fatalf("unexpected error: %s", err)
   165  			}
   166  
   167  			if !got.RawEquals(test.Want) {
   168  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   169  			}
   170  		})
   171  	}
   172  }
   173  
   174  func TestCidrSubnet(t *testing.T) {
   175  	tests := []struct {
   176  		Prefix  cty.Value
   177  		Newbits cty.Value
   178  		Netnum  cty.Value
   179  		Want    cty.Value
   180  		Err     bool
   181  	}{
   182  		{
   183  			cty.StringVal("192.168.2.0/20"),
   184  			cty.NumberIntVal(4),
   185  			cty.NumberIntVal(6),
   186  			cty.StringVal("192.168.6.0/24"),
   187  			false,
   188  		},
   189  		{
   190  			cty.StringVal("fe80::/48"),
   191  			cty.NumberIntVal(16),
   192  			cty.NumberIntVal(6),
   193  			cty.StringVal("fe80:0:0:6::/64"),
   194  			false,
   195  		},
   196  		{ // IPv4 address encoded in IPv6 syntax gets normalized
   197  			cty.StringVal("::ffff:192.168.0.0/112"),
   198  			cty.NumberIntVal(8),
   199  			cty.NumberIntVal(6),
   200  			cty.StringVal("192.168.6.0/24"),
   201  			false,
   202  		},
   203  		{
   204  			cty.StringVal("fe80::/48"),
   205  			cty.NumberIntVal(33),
   206  			cty.NumberIntVal(6),
   207  			cty.StringVal("fe80::3:0:0:0/81"),
   208  			false,
   209  		},
   210  		{
   211  			// We inadvertently inherited a pre-Go1.17 standard library quirk
   212  			// if parsing zero-prefix parts as decimal rather than octal.
   213  			// Go 1.17 resolved that quirk by making zero-prefix invalid, but
   214  			// we've preserved our existing behavior for backward compatibility,
   215  			// on the grounds that these functions are for generating addresses
   216  			// rather than validating or processing them. We do always generate
   217  			// a canonical result regardless of the input, though.
   218  			cty.StringVal("010.001.0.0/24"),
   219  			cty.NumberIntVal(4),
   220  			cty.NumberIntVal(1),
   221  			cty.StringVal("10.1.0.16/28"),
   222  			false,
   223  		},
   224  		{ // not enough bits left
   225  			cty.StringVal("192.168.0.0/30"),
   226  			cty.NumberIntVal(4),
   227  			cty.NumberIntVal(6),
   228  			cty.UnknownVal(cty.String),
   229  			true,
   230  		},
   231  		{ // can't encode 16 in 2 bits
   232  			cty.StringVal("192.168.0.0/168"),
   233  			cty.NumberIntVal(2),
   234  			cty.NumberIntVal(16),
   235  			cty.UnknownVal(cty.String),
   236  			true,
   237  		},
   238  		{ // not a valid CIDR mask
   239  			cty.StringVal("not-a-cidr"),
   240  			cty.NumberIntVal(4),
   241  			cty.NumberIntVal(6),
   242  			cty.UnknownVal(cty.String),
   243  			true,
   244  		},
   245  		{ // can't have an octet >255
   246  			cty.StringVal("10.256.0.0/8"),
   247  			cty.NumberIntVal(4),
   248  			cty.NumberIntVal(6),
   249  			cty.UnknownVal(cty.String),
   250  			true,
   251  		},
   252  		{ // fractions are Not Ok
   253  			cty.StringVal("10.256.0.0/8"),
   254  			cty.NumberFloatVal(2.0 / 3.0),
   255  			cty.NumberFloatVal(.75),
   256  			cty.UnknownVal(cty.String),
   257  			true,
   258  		},
   259  	}
   260  
   261  	for _, test := range tests {
   262  		t.Run(fmt.Sprintf("cidrsubnet(%#v, %#v, %#v)", test.Prefix, test.Newbits, test.Netnum), func(t *testing.T) {
   263  			got, err := CidrSubnet(test.Prefix, test.Newbits, test.Netnum)
   264  
   265  			if test.Err {
   266  				if err == nil {
   267  					t.Fatal("succeeded; want error")
   268  				}
   269  				return
   270  			} else if err != nil {
   271  				t.Fatalf("unexpected error: %s", err)
   272  			}
   273  
   274  			if !got.RawEquals(test.Want) {
   275  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   276  			}
   277  		})
   278  	}
   279  }
   280  func TestCidrSubnets(t *testing.T) {
   281  	tests := []struct {
   282  		Prefix  cty.Value
   283  		Newbits []cty.Value
   284  		Want    cty.Value
   285  		Err     string
   286  	}{
   287  		{
   288  			cty.StringVal("10.0.0.0/21"),
   289  			[]cty.Value{
   290  				cty.NumberIntVal(3),
   291  				cty.NumberIntVal(3),
   292  				cty.NumberIntVal(3),
   293  				cty.NumberIntVal(4),
   294  				cty.NumberIntVal(4),
   295  				cty.NumberIntVal(4),
   296  				cty.NumberIntVal(7),
   297  				cty.NumberIntVal(7),
   298  				cty.NumberIntVal(7),
   299  			},
   300  			cty.ListVal([]cty.Value{
   301  				cty.StringVal("10.0.0.0/24"),
   302  				cty.StringVal("10.0.1.0/24"),
   303  				cty.StringVal("10.0.2.0/24"),
   304  				cty.StringVal("10.0.3.0/25"),
   305  				cty.StringVal("10.0.3.128/25"),
   306  				cty.StringVal("10.0.4.0/25"),
   307  				cty.StringVal("10.0.4.128/28"),
   308  				cty.StringVal("10.0.4.144/28"),
   309  				cty.StringVal("10.0.4.160/28"),
   310  			}),
   311  			``,
   312  		},
   313  		{
   314  			// We inadvertently inherited a pre-Go1.17 standard library quirk
   315  			// if parsing zero-prefix parts as decimal rather than octal.
   316  			// Go 1.17 resolved that quirk by making zero-prefix invalid, but
   317  			// we've preserved our existing behavior for backward compatibility,
   318  			// on the grounds that these functions are for generating addresses
   319  			// rather than validating or processing them. We do always generate
   320  			// a canonical result regardless of the input, though.
   321  			cty.StringVal("010.0.0.0/21"),
   322  			[]cty.Value{
   323  				cty.NumberIntVal(3),
   324  			},
   325  			cty.ListVal([]cty.Value{
   326  				cty.StringVal("10.0.0.0/24"),
   327  			}),
   328  			``,
   329  		},
   330  		{
   331  			cty.StringVal("10.0.0.0/30"),
   332  			[]cty.Value{
   333  				cty.NumberIntVal(1),
   334  				cty.NumberIntVal(3),
   335  			},
   336  			cty.UnknownVal(cty.List(cty.String)),
   337  			`would extend prefix to 33 bits, which is too long for an IPv4 address`,
   338  		},
   339  		{
   340  			cty.StringVal("10.0.0.0/8"),
   341  			[]cty.Value{
   342  				cty.NumberIntVal(1),
   343  				cty.NumberIntVal(1),
   344  				cty.NumberIntVal(1),
   345  			},
   346  			cty.UnknownVal(cty.List(cty.String)),
   347  			`not enough remaining address space for a subnet with a prefix of 9 bits after 10.128.0.0/9`,
   348  		},
   349  		{
   350  			cty.StringVal("10.0.0.0/8"),
   351  			[]cty.Value{
   352  				cty.NumberIntVal(1),
   353  				cty.NumberIntVal(0),
   354  			},
   355  			cty.UnknownVal(cty.List(cty.String)),
   356  			`must extend prefix by at least one bit`,
   357  		},
   358  		{
   359  			cty.StringVal("10.0.0.0/8"),
   360  			[]cty.Value{
   361  				cty.NumberIntVal(1),
   362  				cty.NumberIntVal(-1),
   363  			},
   364  			cty.UnknownVal(cty.List(cty.String)),
   365  			`must extend prefix by at least one bit`,
   366  		},
   367  		{
   368  			cty.StringVal("fe80::/48"),
   369  			[]cty.Value{
   370  				cty.NumberIntVal(1),
   371  				cty.NumberIntVal(33),
   372  			},
   373  			cty.UnknownVal(cty.List(cty.String)),
   374  			`may not extend prefix by more than 32 bits`,
   375  		},
   376  	}
   377  
   378  	for _, test := range tests {
   379  		t.Run(fmt.Sprintf("cidrsubnets(%#v, %#v)", test.Prefix, test.Newbits), func(t *testing.T) {
   380  			got, err := CidrSubnets(test.Prefix, test.Newbits...)
   381  			wantErr := test.Err != ""
   382  
   383  			if wantErr {
   384  				if err == nil {
   385  					t.Fatal("succeeded; want error")
   386  				}
   387  				if err.Error() != test.Err {
   388  					t.Fatalf("wrong error\ngot:  %s\nwant: %s", err.Error(), test.Err)
   389  				}
   390  				return
   391  			} else if err != nil {
   392  				t.Fatalf("unexpected error: %s", err)
   393  			}
   394  
   395  			if !got.RawEquals(test.Want) {
   396  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   397  			}
   398  		})
   399  	}
   400  }
   401  
   402  func TestCidrContains(t *testing.T) {
   403  	noError := func(err error) bool { return err == nil }
   404  
   405  	tests := []struct {
   406  		Prefix  cty.Value
   407  		Address cty.Value
   408  		Want    cty.Value
   409  		ErrFn   func(error) bool
   410  	}{
   411  		{
   412  			// IPv4, contained (IP).
   413  			cty.StringVal("192.168.2.0/20"),
   414  			cty.StringVal("192.168.2.1"),
   415  			cty.True,
   416  			noError,
   417  		},
   418  		{
   419  			// IPv4, contained (CIDR).
   420  			cty.StringVal("192.168.2.0/20"),
   421  			cty.StringVal("192.168.2.0/22"),
   422  			cty.True,
   423  			noError,
   424  		},
   425  		{
   426  			// IPv4, not contained.
   427  			cty.StringVal("192.168.2.0/20"),
   428  			cty.StringVal("192.126.2.1"),
   429  			cty.False,
   430  			noError,
   431  		},
   432  		{
   433  			// IPv4, not contained (CIDR).
   434  			cty.StringVal("192.168.2.0/20"),
   435  			cty.StringVal("192.126.2.0/18"),
   436  			cty.False,
   437  			noError,
   438  		},
   439  		{
   440  			// IPv6, contained.
   441  			cty.StringVal("fe80::/48"),
   442  			cty.StringVal("fe80::1"),
   443  			cty.True,
   444  			noError,
   445  		},
   446  		{
   447  			// IPv6, not contained.
   448  			cty.StringVal("fe80::/48"),
   449  			cty.StringVal("fe81::1"),
   450  			cty.False,
   451  			noError,
   452  		},
   453  		{
   454  			// Address family mismatch: IPv4 containing_prefix, IPv6 contained_ip_or_prefix (IP).
   455  			cty.StringVal("192.168.2.0/20"),
   456  			cty.StringVal("fe80::1"),
   457  			cty.NilVal,
   458  			func(err error) bool {
   459  				return err != nil && err.Error() == "address family mismatch: 192.168.2.0/20 vs. fe80::1"
   460  			},
   461  		},
   462  		{
   463  			// Address family mismatch: IPv4 containing_prefix, IPv6 contained_ip_or_prefix (prefix).
   464  			cty.StringVal("192.168.2.0/20"),
   465  			cty.StringVal("fe80::/24"),
   466  			cty.NilVal,
   467  			func(err error) bool {
   468  				return err != nil && err.Error() == "address family mismatch: 192.168.2.0/20 vs. fe80::/24"
   469  			},
   470  		},
   471  		{
   472  			// Address family mismatch: IPv6 containing_prefix, IPv4 contained_ip_or_prefix (IP).
   473  			cty.StringVal("fe80::/48"),
   474  			cty.StringVal("192.168.2.1"),
   475  			cty.NilVal,
   476  			func(err error) bool {
   477  				return err != nil && err.Error() == "address family mismatch: fe80::/48 vs. 192.168.2.1"
   478  			},
   479  		},
   480  		{
   481  			// Address family mismatch: IPv6 containing_prefix, IPv4 contained_ip_or_prefix (prefix).
   482  			cty.StringVal("fe80::/48"),
   483  			cty.StringVal("192.168.2.0/20"),
   484  			cty.NilVal,
   485  			func(err error) bool {
   486  				return err != nil && err.Error() == "address family mismatch: fe80::/48 vs. 192.168.2.0/20"
   487  			},
   488  		},
   489  		{
   490  			// Input error: invalid CIDR address.
   491  			cty.StringVal("not-a-cidr"),
   492  			cty.StringVal("192.168.2.1"),
   493  			cty.NilVal,
   494  			func(err error) bool {
   495  				return err != nil && err.Error() == "invalid CIDR address: not-a-cidr"
   496  			},
   497  		},
   498  		{
   499  			// Input error: invalid IP address.
   500  			cty.StringVal("192.168.2.0/20"),
   501  			cty.StringVal("not-an-address"),
   502  			cty.NilVal,
   503  			func(err error) bool {
   504  				return err != nil && err.Error() == "invalid IP address or prefix: not-an-address"
   505  			},
   506  		},
   507  	}
   508  
   509  	for _, test := range tests {
   510  		t.Run(fmt.Sprintf("cidrcontains(%#v, %#v)", test.Prefix, test.Address), func(t *testing.T) {
   511  			got, err := CidrContains(test.Prefix, test.Address)
   512  
   513  			if !test.ErrFn(err) {
   514  				t.Errorf("unexpected error: %v", err)
   515  			}
   516  
   517  			if !got.RawEquals(test.Want) {
   518  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   519  			}
   520  		})
   521  	}
   522  }