go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/roller-configurator/validate_test.go (about)

     1  // Copyright 2023 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"context"
     9  	"os"
    10  	"path/filepath"
    11  	"testing"
    12  
    13  	"go.fuchsia.dev/infra/cmd/roller-configurator/proto"
    14  )
    15  
    16  var filesWithGitmodules = map[string]string{
    17  	".gitmodules": `
    18  		[submodule "path/to/submodule"]
    19  		url = "https://example.com/asubmodule"
    20  	`,
    21  }
    22  
    23  var filesWithJiriManifest = map[string]string{
    24  	"path/to/jiri_manifest": `
    25  		<?xml version="1.0" encoding="UTF-8"?>
    26  		<manifest>
    27  			<projects>
    28  				<project name="foo-project"
    29  						 remote="https://example.com/jiri-project"/>
    30  			</projects>
    31  			<packages>
    32  				<package name="package1"/>
    33  				<package name="package2"/>
    34  			</packages>
    35  		</manifest>
    36  	`,
    37  }
    38  
    39  func TestValidate_valid(t *testing.T) {
    40  	t.Parallel()
    41  
    42  	testCases := []struct {
    43  		name   string
    44  		config *proto.Config
    45  		files  map[string]string
    46  	}{
    47  		{
    48  			name:   "empty",
    49  			config: &proto.Config{},
    50  		},
    51  		{
    52  			name: "submodule with interval schedule",
    53  			config: &proto.Config{
    54  				Rollers: []*proto.Roller{
    55  					{
    56  						ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{
    57  							Path: "path/to/submodule",
    58  						}},
    59  						Schedule: "with 24h interval",
    60  					},
    61  				},
    62  			},
    63  			files: filesWithGitmodules,
    64  		},
    65  		{
    66  			name: "cipd ensure file with cron schedule",
    67  			config: &proto.Config{
    68  				Rollers: []*proto.Roller{
    69  					{
    70  						ToRoll: &proto.Roller_CipdEnsureFile{CipdEnsureFile: &proto.CIPDEnsureFile{
    71  							Path: "path/to/cipd.ensure",
    72  							Ref:  "foo",
    73  						}},
    74  						Schedule: "0/20 * * * *",
    75  					},
    76  				},
    77  			},
    78  			files: map[string]string{
    79  				"path/to/cipd.ensure": ``,
    80  			},
    81  		},
    82  		{
    83  			name: "jiri project with notify emails",
    84  			config: &proto.Config{
    85  				Rollers: []*proto.Roller{
    86  					{
    87  						ToRoll: &proto.Roller_JiriProject{JiriProject: &proto.JiriProject{
    88  							Manifest: "path/to/jiri_manifest",
    89  							Project:  "foo-project",
    90  						}},
    91  						NotifyEmails: []string{
    92  							"foo@example.com",
    93  							"bar@example.com",
    94  						},
    95  					},
    96  				},
    97  				DefaultCheckoutJiriManifest: "manifest",
    98  			},
    99  			files: filesWithJiriManifest,
   100  		},
   101  		{
   102  			name: "jiri packages",
   103  			config: &proto.Config{
   104  				Rollers: []*proto.Roller{
   105  					{
   106  						ToRoll: &proto.Roller_JiriPackages{JiriPackages: &proto.JiriPackages{
   107  							Manifests: []*proto.JiriPackages_Manifest{
   108  								{
   109  									Path: "path/to/jiri_manifest",
   110  									Packages: []string{
   111  										"package1",
   112  										"package2",
   113  									},
   114  								},
   115  							},
   116  						}},
   117  					},
   118  				},
   119  				DefaultCheckoutJiriManifest: "manifest",
   120  			},
   121  			files: filesWithJiriManifest,
   122  		},
   123  	}
   124  
   125  	for _, tc := range testCases {
   126  		t.Run(tc.name, func(t *testing.T) {
   127  			t.Parallel()
   128  
   129  			repoRoot := t.TempDir()
   130  			writeFiles(t, repoRoot, tc.files)
   131  
   132  			err := validate(context.Background(), repoRoot, tc.config)
   133  			if err != nil {
   134  				t.Fatal(err)
   135  			}
   136  		})
   137  	}
   138  }
   139  
   140  func TestValidate_invalid(t *testing.T) {
   141  	t.Parallel()
   142  
   143  	testCases := []struct {
   144  		name   string
   145  		config *proto.Config
   146  		err    string
   147  		files  map[string]string
   148  	}{
   149  		{
   150  			name: "unnecessary default_checkout_jiri_manifest",
   151  			config: &proto.Config{
   152  				// Shouldn't be set unless `Rollers` contains any Jiri rollers.
   153  				DefaultCheckoutJiriManifest: "manifest",
   154  			},
   155  			err: "default_checkout_jiri_manifest need not be set",
   156  		},
   157  		{
   158  			name: "no entity to roll",
   159  			config: &proto.Config{
   160  				Rollers: []*proto.Roller{
   161  					{},
   162  				},
   163  			},
   164  			err: "entry 0 is missing an entity to roll",
   165  		},
   166  		{
   167  			name: "invalid interval schedule",
   168  			config: &proto.Config{
   169  				Rollers: []*proto.Roller{
   170  					{
   171  						ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{
   172  							Path: "path/to/submodule",
   173  						}},
   174  						Schedule: "with blah",
   175  					},
   176  				},
   177  			},
   178  			files: filesWithGitmodules,
   179  			err:   `invalid schedule "with blah"`,
   180  		},
   181  		{
   182  			name: "invalid interval schedule duration",
   183  			config: &proto.Config{
   184  				Rollers: []*proto.Roller{
   185  					{
   186  						ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{
   187  							Path: "path/to/submodule",
   188  						}},
   189  						Schedule: "with 6f interval",
   190  					},
   191  				},
   192  			},
   193  			files: filesWithGitmodules,
   194  			err:   `invalid duration in schedule "with 6f interval"`,
   195  		},
   196  		{
   197  			name: "invalid cron schedule",
   198  			config: &proto.Config{
   199  				Rollers: []*proto.Roller{
   200  					{
   201  						ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{
   202  							Path: "path/to/submodule",
   203  						}},
   204  						Schedule: "foo * * * *",
   205  					},
   206  				},
   207  			},
   208  			files: filesWithGitmodules,
   209  			err:   `invalid cron schedule "foo * * * *"`,
   210  		},
   211  		{
   212  			name: "invalid notify email",
   213  			config: &proto.Config{
   214  				Rollers: []*proto.Roller{
   215  					{
   216  						ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{
   217  							Path: "path/to/submodule",
   218  						}},
   219  						NotifyEmails: []string{
   220  							"valid@example.com",
   221  							"not-an-email",
   222  						},
   223  					},
   224  				},
   225  			},
   226  			files: filesWithGitmodules,
   227  			err:   `invalid email "not-an-email"`,
   228  		},
   229  		{
   230  			name: "submodule with no .gitmodules file",
   231  			config: &proto.Config{
   232  				Rollers: []*proto.Roller{
   233  					{
   234  						ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{
   235  							Path: "path/to/submodule",
   236  						}},
   237  					},
   238  				},
   239  			},
   240  			err: "no .gitmodules file in repository root",
   241  		},
   242  		{
   243  			name: "submodule with invalid .gitmodules file",
   244  			config: &proto.Config{
   245  				Rollers: []*proto.Roller{
   246  					{
   247  						ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{
   248  							Path: "path/to/submodule",
   249  						}},
   250  					},
   251  				},
   252  			},
   253  			files: map[string]string{
   254  				".gitmodules": `invalid`,
   255  			},
   256  			err: "invalid `git config --list --file .gitmodules` line: \"invalid\"",
   257  		},
   258  		{
   259  			name: "submodule not listed in .gitmodules",
   260  			config: &proto.Config{
   261  				Rollers: []*proto.Roller{
   262  					{
   263  						ToRoll: &proto.Roller_Submodule{Submodule: &proto.GitSubmodule{
   264  							Path: "path/to/INVALID",
   265  						}},
   266  					},
   267  				},
   268  			},
   269  			files: map[string]string{
   270  				".gitmodules": `
   271  				[submodule "path/to/submodule"]
   272  					url = "https://example.com/asubmodule"
   273  				`,
   274  			},
   275  			err: `no such submodule "path/to/INVALID" listed in .gitmodules`,
   276  		},
   277  		{
   278  			name: "missing CIPD ensure file",
   279  			config: &proto.Config{
   280  				Rollers: []*proto.Roller{
   281  					{
   282  						ToRoll: &proto.Roller_CipdEnsureFile{CipdEnsureFile: &proto.CIPDEnsureFile{
   283  							Path: "path/to/cipd.ensure",
   284  							Ref:  "foo",
   285  						}},
   286  					},
   287  				},
   288  			},
   289  			err: "no such file: path/to/cipd.ensure",
   290  		},
   291  		{
   292  			name: "missing jiri project manifest",
   293  			config: &proto.Config{
   294  				Rollers: []*proto.Roller{
   295  					{
   296  						ToRoll: &proto.Roller_JiriProject{JiriProject: &proto.JiriProject{
   297  							Manifest: "path/to/manifest",
   298  							Project:  "project-name",
   299  						}},
   300  					},
   301  				},
   302  			},
   303  			err: "no such file: path/to/manifest",
   304  		},
   305  		{
   306  			name: "missing jiri package manifest",
   307  			config: &proto.Config{
   308  				Rollers: []*proto.Roller{
   309  					{
   310  						ToRoll: &proto.Roller_JiriPackages{JiriPackages: &proto.JiriPackages{
   311  							Manifests: []*proto.JiriPackages_Manifest{
   312  								{
   313  									Path: "path/to/manifest",
   314  									Packages: []string{
   315  										"package1",
   316  										"package2",
   317  									},
   318  								},
   319  							},
   320  						}},
   321  					},
   322  				},
   323  			},
   324  			err: "no such file: path/to/manifest",
   325  		},
   326  		{
   327  			name: "invalid jiri project",
   328  			config: &proto.Config{
   329  				Rollers: []*proto.Roller{
   330  					{
   331  						ToRoll: &proto.Roller_JiriProject{JiriProject: &proto.JiriProject{
   332  							Manifest: "path/to/jiri_manifest",
   333  							Project:  "not-a-project",
   334  						}},
   335  					},
   336  				},
   337  			},
   338  			files: filesWithJiriManifest,
   339  			err:   `no project "not-a-project" in manifest "path/to/jiri_manifest"`,
   340  		},
   341  		{
   342  			name: "invalid jiri package",
   343  			config: &proto.Config{
   344  				Rollers: []*proto.Roller{
   345  					{
   346  						ToRoll: &proto.Roller_JiriPackages{JiriPackages: &proto.JiriPackages{
   347  							Manifests: []*proto.JiriPackages_Manifest{
   348  								{
   349  									Path: "path/to/jiri_manifest",
   350  									Packages: []string{
   351  										"bad-package1",
   352  										"bad-package2",
   353  									},
   354  								},
   355  							},
   356  						}},
   357  					},
   358  				},
   359  			},
   360  			files: filesWithJiriManifest,
   361  			err:   `no package "bad-package1" in manifest "path/to/jiri_manifest"`,
   362  		},
   363  		{
   364  			name: "jiri project roller with no default checkout manifest",
   365  			config: &proto.Config{
   366  				Rollers: []*proto.Roller{
   367  					{
   368  						ToRoll: &proto.Roller_JiriProject{JiriProject: &proto.JiriProject{
   369  							Manifest: "path/to/jiri_manifest",
   370  							Project:  "foo-project",
   371  						}},
   372  					},
   373  				},
   374  			},
   375  			files: filesWithJiriManifest,
   376  			err:   `default_checkout_jiri_manifest is required to enable jiri rollers`,
   377  		},
   378  		{
   379  			name: "jiri package roller with no default checkout manifest",
   380  			config: &proto.Config{
   381  				Rollers: []*proto.Roller{
   382  					{
   383  						ToRoll: &proto.Roller_JiriPackages{JiriPackages: &proto.JiriPackages{
   384  							Manifests: []*proto.JiriPackages_Manifest{
   385  								{
   386  									Path: "path/to/jiri_manifest",
   387  									Packages: []string{
   388  										"package1",
   389  										"package2",
   390  									},
   391  								},
   392  							},
   393  						}},
   394  					},
   395  				},
   396  			},
   397  			files: filesWithJiriManifest,
   398  			err:   `default_checkout_jiri_manifest is required to enable jiri rollers`,
   399  		},
   400  	}
   401  
   402  	for _, tc := range testCases {
   403  		t.Run(tc.name, func(t *testing.T) {
   404  			t.Parallel()
   405  
   406  			repoRoot := t.TempDir()
   407  			writeFiles(t, repoRoot, tc.files)
   408  
   409  			err := validate(context.Background(), repoRoot, tc.config)
   410  			if err == nil {
   411  				t.Fatalf("Expected an error: %s", tc.err)
   412  			}
   413  			if err.Error() != tc.err {
   414  				t.Fatalf("Got error %q, expected %q", err, tc.err)
   415  			}
   416  		})
   417  	}
   418  }
   419  
   420  func writeFiles(t *testing.T, rootDir string, files map[string]string) {
   421  	for path, contents := range files {
   422  		abspath := filepath.Join(rootDir, path)
   423  		if err := os.MkdirAll(filepath.Dir(abspath), 0o700); err != nil {
   424  			t.Fatal(err)
   425  		}
   426  		if err := os.WriteFile(abspath, []byte(contents), 0o600); err != nil {
   427  			t.Fatal(err)
   428  		}
   429  	}
   430  }