github.com/letsencrypt/boulder@v0.20251208.0/cmd/shell_test.go (about) 1 package cmd 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "os" 8 "os/exec" 9 "runtime" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/prometheus/client_golang/prometheus" 15 16 "github.com/letsencrypt/boulder/config" 17 "github.com/letsencrypt/boulder/core" 18 blog "github.com/letsencrypt/boulder/log" 19 "github.com/letsencrypt/boulder/test" 20 ) 21 22 var ( 23 validPAConfig = []byte(`{ 24 "dbConnect": "dummyDBConnect", 25 "enforcePolicyWhitelist": false, 26 "challenges": { "http-01": true }, 27 "identifiers": { "dns": true, "ip": true } 28 }`) 29 invalidPAConfig = []byte(`{ 30 "dbConnect": "dummyDBConnect", 31 "enforcePolicyWhitelist": false, 32 "challenges": { "nonsense": true }, 33 "identifiers": { "openpgp": true } 34 }`) 35 noChallengesIdentsPAConfig = []byte(`{ 36 "dbConnect": "dummyDBConnect", 37 "enforcePolicyWhitelist": false 38 }`) 39 emptyChallengesIdentsPAConfig = []byte(`{ 40 "dbConnect": "dummyDBConnect", 41 "enforcePolicyWhitelist": false, 42 "challenges": {}, 43 "identifiers": {} 44 }`) 45 ) 46 47 func TestPAConfigUnmarshal(t *testing.T) { 48 var pc1 PAConfig 49 err := json.Unmarshal(validPAConfig, &pc1) 50 test.AssertNotError(t, err, "Failed to unmarshal PAConfig") 51 test.AssertNotError(t, pc1.CheckChallenges(), "Flagged valid challenges as bad") 52 test.AssertNotError(t, pc1.CheckIdentifiers(), "Flagged valid identifiers as bad") 53 54 var pc2 PAConfig 55 err = json.Unmarshal(invalidPAConfig, &pc2) 56 test.AssertNotError(t, err, "Failed to unmarshal PAConfig") 57 test.AssertError(t, pc2.CheckChallenges(), "Considered invalid challenges as good") 58 test.AssertError(t, pc2.CheckIdentifiers(), "Considered invalid identifiers as good") 59 60 var pc3 PAConfig 61 err = json.Unmarshal(noChallengesIdentsPAConfig, &pc3) 62 test.AssertNotError(t, err, "Failed to unmarshal PAConfig") 63 test.AssertError(t, pc3.CheckChallenges(), "Disallow empty challenges map") 64 test.AssertNotError(t, pc3.CheckIdentifiers(), "Disallowed empty identifiers map") 65 66 var pc4 PAConfig 67 err = json.Unmarshal(emptyChallengesIdentsPAConfig, &pc4) 68 test.AssertNotError(t, err, "Failed to unmarshal PAConfig") 69 test.AssertError(t, pc4.CheckChallenges(), "Disallow empty challenges map") 70 test.AssertNotError(t, pc4.CheckIdentifiers(), "Disallowed empty identifiers map") 71 } 72 73 func TestMysqlLogger(t *testing.T) { 74 log := blog.UseMock() 75 mLog := mysqlLogger{log} 76 77 testCases := []struct { 78 args []any 79 expected string 80 }{ 81 { 82 []any{nil}, 83 `ERR: [AUDIT] [mysql] <nil>`, 84 }, 85 { 86 []any{""}, 87 `ERR: [AUDIT] [mysql] `, 88 }, 89 { 90 []any{"Sup ", 12345, " Sup sup"}, 91 `ERR: [AUDIT] [mysql] Sup 12345 Sup sup`, 92 }, 93 } 94 95 for _, tc := range testCases { 96 // mysqlLogger proxies blog.AuditLogger to provide a Print() method 97 mLog.Print(tc.args...) 98 logged := log.GetAll() 99 // Calling Print should produce the expected output 100 test.AssertEquals(t, len(logged), 1) 101 test.AssertEquals(t, logged[0], tc.expected) 102 log.Clear() 103 } 104 } 105 106 func TestCaptureStdlibLog(t *testing.T) { 107 logger := blog.UseMock() 108 oldDest := log.Writer() 109 defer func() { 110 log.SetOutput(oldDest) 111 }() 112 log.SetOutput(logWriter{logger}) 113 log.Print("thisisatest") 114 results := logger.GetAllMatching("thisisatest") 115 if len(results) != 1 { 116 t.Fatalf("Expected logger to receive 'thisisatest', got: %s", 117 strings.Join(logger.GetAllMatching(".*"), "\n")) 118 } 119 } 120 121 func TestVersionString(t *testing.T) { 122 core.BuildID = "TestBuildID" 123 core.BuildTime = "RightNow!" 124 core.BuildHost = "Localhost" 125 126 versionStr := VersionString() 127 expected := fmt.Sprintf("Versions: cmd.test=(TestBuildID RightNow!) Golang=(%s) BuildHost=(Localhost)", runtime.Version()) 128 test.AssertEquals(t, versionStr, expected) 129 } 130 131 func TestReadConfigFile(t *testing.T) { 132 err := ReadConfigFile("", nil) 133 test.AssertError(t, err, "ReadConfigFile('') did not error") 134 135 type config struct { 136 GRPC *GRPCClientConfig 137 TLS *TLSConfig 138 } 139 var c config 140 err = ReadConfigFile("../test/config/health-checker.json", &c) 141 test.AssertNotError(t, err, "ReadConfigFile(../test/config/health-checker.json) errored") 142 test.AssertEquals(t, c.GRPC.Timeout.Duration, 1*time.Second) 143 } 144 145 func TestLogWriter(t *testing.T) { 146 mock := blog.UseMock() 147 lw := logWriter{mock} 148 _, _ = lw.Write([]byte("hi\n")) 149 lines := mock.GetAllMatching(".*") 150 test.AssertEquals(t, len(lines), 1) 151 test.AssertEquals(t, lines[0], "INFO: hi") 152 } 153 154 func TestGRPCLoggerWarningFilter(t *testing.T) { 155 m := blog.NewMock() 156 l := grpcLogger{m} 157 l.Warningln("asdf", "qwer") 158 lines := m.GetAllMatching(".*") 159 test.AssertEquals(t, len(lines), 1) 160 161 m = blog.NewMock() 162 l = grpcLogger{m} 163 l.Warningln("Server.processUnaryRPC failed to write status: connection error: desc = \"transport is closing\"") 164 lines = m.GetAllMatching(".*") 165 test.AssertEquals(t, len(lines), 0) 166 } 167 168 func Test_newVersionCollector(t *testing.T) { 169 // 'buildTime' 170 core.BuildTime = core.Unspecified 171 version := newVersionCollector() 172 // Default 'Unspecified' should emit 'Unspecified'. 173 test.AssertMetricWithLabelsEquals(t, version, prometheus.Labels{"buildTime": core.Unspecified}, 1) 174 // Parsable UnixDate should emit UnixTime. 175 now := time.Now().UTC() 176 core.BuildTime = now.Format(time.UnixDate) 177 version = newVersionCollector() 178 test.AssertMetricWithLabelsEquals(t, version, prometheus.Labels{"buildTime": now.Format(time.RFC3339)}, 1) 179 // Unparsable timestamp should emit 'Unsparsable'. 180 core.BuildTime = "outta time" 181 version = newVersionCollector() 182 test.AssertMetricWithLabelsEquals(t, version, prometheus.Labels{"buildTime": "Unparsable"}, 1) 183 184 // 'buildId' 185 expectedBuildID := "TestBuildId" 186 core.BuildID = expectedBuildID 187 version = newVersionCollector() 188 test.AssertMetricWithLabelsEquals(t, version, prometheus.Labels{"buildId": expectedBuildID}, 1) 189 190 // 'goVersion' 191 test.AssertMetricWithLabelsEquals(t, version, prometheus.Labels{"goVersion": runtime.Version()}, 1) 192 } 193 194 func loadConfigFile(t *testing.T, path string) *os.File { 195 cf, err := os.Open(path) 196 if err != nil { 197 t.Fatal(err) 198 } 199 return cf 200 } 201 202 func TestFailedConfigValidation(t *testing.T) { 203 type FooConfig struct { 204 VitalValue string `yaml:"vitalValue" validate:"required"` 205 VoluntarilyVoid string `yaml:"voluntarilyVoid"` 206 VisciouslyVetted string `yaml:"visciouslyVetted" validate:"omitempty,endswith=baz"` 207 VolatileVagary config.Duration `yaml:"volatileVagary" validate:"required,lte=120s"` 208 VernalVeil config.Duration `yaml:"vernalVeil" validate:"required"` 209 } 210 211 // Violates 'endswith' tag JSON. 212 cf := loadConfigFile(t, "testdata/1_missing_endswith.json") 213 defer cf.Close() 214 err := ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf) 215 test.AssertError(t, err, "Expected validation error") 216 test.AssertContains(t, err.Error(), "'endswith'") 217 218 // Violates 'endswith' tag YAML. 219 cf = loadConfigFile(t, "testdata/1_missing_endswith.yaml") 220 defer cf.Close() 221 err = ValidateYAMLConfig(&ConfigValidator{&FooConfig{}, nil}, cf) 222 test.AssertError(t, err, "Expected validation error") 223 test.AssertContains(t, err.Error(), "'endswith'") 224 225 // Violates 'required' tag JSON. 226 cf = loadConfigFile(t, "testdata/2_missing_required.json") 227 defer cf.Close() 228 err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf) 229 test.AssertError(t, err, "Expected validation error") 230 test.AssertContains(t, err.Error(), "'required'") 231 232 // Violates 'required' tag YAML. 233 cf = loadConfigFile(t, "testdata/2_missing_required.yaml") 234 defer cf.Close() 235 err = ValidateYAMLConfig(&ConfigValidator{&FooConfig{}, nil}, cf) 236 test.AssertError(t, err, "Expected validation error") 237 test.AssertContains(t, err.Error(), "'required'") 238 239 // Violates 'lte' tag JSON for config.Duration type. 240 cf = loadConfigFile(t, "testdata/3_configDuration_too_darn_big.json") 241 defer cf.Close() 242 err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf) 243 test.AssertError(t, err, "Expected validation error") 244 test.AssertContains(t, err.Error(), "'lte'") 245 246 // Violates 'lte' tag JSON for config.Duration type. 247 cf = loadConfigFile(t, "testdata/3_configDuration_too_darn_big.json") 248 defer cf.Close() 249 err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf) 250 test.AssertError(t, err, "Expected validation error") 251 test.AssertContains(t, err.Error(), "'lte'") 252 253 // Incorrect value for the config.Duration type. 254 cf = loadConfigFile(t, "testdata/4_incorrect_data_for_type.json") 255 defer cf.Close() 256 err = ValidateJSONConfig(&ConfigValidator{&FooConfig{}, nil}, cf) 257 test.AssertError(t, err, "Expected error") 258 test.AssertContains(t, err.Error(), "missing unit in duration") 259 260 // Incorrect value for the config.Duration type. 261 cf = loadConfigFile(t, "testdata/4_incorrect_data_for_type.yaml") 262 defer cf.Close() 263 err = ValidateYAMLConfig(&ConfigValidator{&FooConfig{}, nil}, cf) 264 test.AssertError(t, err, "Expected error") 265 test.AssertContains(t, err.Error(), "missing unit in duration") 266 } 267 268 func TestFailExit(t *testing.T) { 269 // Test that when Fail is called with a `defer AuditPanic()`, 270 // the program exits with a non-zero exit code and logs 271 // the result (but not stack trace). 272 // Inspired by https://go.dev/talks/2014/testing.slide#23 273 if os.Getenv("TIME_TO_DIE") == "1" { 274 defer AuditPanic() 275 Fail("tears in the rain") 276 return 277 } 278 279 cmd := exec.Command(os.Args[0], "-test.run=TestFailExit") 280 cmd.Env = append(os.Environ(), "TIME_TO_DIE=1") 281 output, err := cmd.CombinedOutput() 282 test.AssertError(t, err, "running a failing program") 283 test.AssertContains(t, string(output), "[AUDIT] tears in the rain") 284 // "goroutine" usually shows up in stack traces, so we check it 285 // to make sure we didn't print a stack trace. 286 test.AssertNotContains(t, string(output), "goroutine") 287 } 288 289 func testPanicStackTraceHelper() { 290 var x *int 291 *x = 1 //nolint: govet // Purposeful nil pointer dereference to trigger a panic 292 } 293 294 func TestPanicStackTrace(t *testing.T) { 295 // Test that when a nil pointer dereference is hit after a 296 // `defer AuditPanic()`, the program exits with a non-zero 297 // exit code and prints the result (but not stack trace). 298 // Inspired by https://go.dev/talks/2014/testing.slide#23 299 if os.Getenv("AT_THE_DISCO") == "1" { 300 defer AuditPanic() 301 testPanicStackTraceHelper() 302 return 303 } 304 305 cmd := exec.Command(os.Args[0], "-test.run=TestPanicStackTrace") 306 cmd.Env = append(os.Environ(), "AT_THE_DISCO=1") 307 output, err := cmd.CombinedOutput() 308 test.AssertError(t, err, "running a failing program") 309 test.AssertContains(t, string(output), "nil pointer dereference") 310 test.AssertContains(t, string(output), "Stack Trace") 311 test.AssertContains(t, string(output), "cmd/shell_test.go:") 312 }