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  }