...

Source file src/github.com/letsencrypt/boulder/cmd/shell_test.go

Documentation: github.com/letsencrypt/boulder/cmd

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

View as plain text