| author | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-10-03 09:34:35 UTC |
| committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-10-03 09:34:35 UTC |
| parent | 8d763f450d6bc9ba47bfc627b8b8ced056301b5a |
| .gitignore | +3 | -0 |
| chasquid_test.go | +1 | -2 |
| internal/aliases/aliases_test.go | +10 | -17 |
| internal/auth/auth_test.go | +71 | -74 |
| internal/config/config_test.go | +10 | -16 |
| internal/courier/fakeserver_test.go | +3 | -15 |
| internal/courier/mda_test.go | +1 | -4 |
| internal/courier/smtp_test.go | +9 | -24 |
| internal/dkim/context_test.go | +5 | -5 |
| internal/dkim/dns_test.go | +2 | -2 |
| internal/dkim/file_test.go | +1 | -1 |
| internal/dkim/sign_test.go | +4 | -5 |
| internal/dkim/verify_test.go | +2 | -2 |
| internal/domaininfo/domaininfo_test.go | +5 | -10 |
| internal/dovecot/dovecot_test.go | +1 | -4 |
| internal/localrpc/client_test.go | +13 | -12 |
| internal/localrpc/e2e_test.go | +8 | -33 |
| internal/localrpc/localrpc.go | +10 | -5 |
| internal/nettrace/bench_test.go | +2 | -3 |
| internal/nettrace/context_test.go | +3 | -4 |
| internal/normalize/normalize_test.go | +2 | -4 |
| internal/protoio/protoio_test.go | +10 | -17 |
| internal/queue/queue_test.go | +239 | -217 |
| internal/safeio/safeio_test.go | +10 | -10 |
| internal/smtpsrv/conn_test.go | +1 | -11 |
| internal/smtpsrv/server_test.go | +22 | -39 |
| internal/sts/sts_test.go | +16 | -18 |
| internal/testlib/testlib.go | +9 | -69 |
| internal/testlib/testlib_test.go | +9 | -90 |
| internal/userdb/userdb_test.go | +7 | -44 |
diff --git a/.gitignore b/.gitignore index c2d4851..e24435a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ chasquid.test # Exclude any .pem files, to prevent accidentally including test keys and # certificates. *.pem + +# Test artifacts, saved when running "go test -artifacts". +_artifacts/ diff --git a/chasquid_test.go b/chasquid_test.go index 18d9184..ede5de0 100644 --- a/chasquid_test.go +++ b/chasquid_test.go @@ -21,8 +21,7 @@ func mustMkdir(t *testing.T, path string) string { } func TestFindCertKey(t *testing.T) { - tmpDir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, tmpDir) + tmpDir := t.ArtifactDir() cases := []struct { name string diff --git a/internal/aliases/aliases_test.go b/internal/aliases/aliases_test.go index a3b0158..314a0cd 100644 --- a/internal/aliases/aliases_test.go +++ b/internal/aliases/aliases_test.go @@ -5,11 +5,11 @@ import ( "errors" "os" "os/exec" - "reflect" "strings" "testing" "blitiri.com.ar/go/chasquid/internal/trace" + "github.com/google/go-cmp/cmp" ) type Cases []struct { @@ -29,9 +29,8 @@ func (cases Cases) check(t *testing.T, r *Resolver) { t.Errorf("case %q: expected error %v, got %v", c.addr, c.err, err) } - if !reflect.DeepEqual(got, c.expect) { - t.Errorf("case %q: got %+v, expected %+v", - c.addr, got, c.expect) + if diff := cmp.Diff(c.expect, got); diff != "" { + t.Errorf("case %q: mismatch (-want +got):\n%s", c.addr, diff) } } } @@ -507,7 +506,8 @@ func TestTooMuchRecursionOnCatchAll(t *testing.T) { } func mustWriteFile(t *testing.T, content string) string { - f, err := os.CreateTemp("", "aliases_test") + t.Helper() + f, err := os.CreateTemp(t.TempDir(), "aliases_test") if err != nil { t.Fatalf("failed to get temp file: %v", err) } @@ -555,7 +555,6 @@ func TestAddFile(t *testing.T) { for _, c := range cases { fname := mustWriteFile(t, c.contents) - defer os.Remove(fname) resolver := NewResolver(allUsersExist) _, err := resolver.AddAliasesFile("dom", fname) @@ -568,8 +567,8 @@ func TestAddFile(t *testing.T) { t.Errorf("case %q, got error: %v", c.contents, err) continue } - if !reflect.DeepEqual(got, c.expected) { - t.Errorf("case %q, got %v, expected %v", c.contents, got, c.expected) + if diff := cmp.Diff(c.expected, got); diff != "" { + t.Errorf("case %q: mismatch (-want +got):\n%s", c.contents, diff) } } @@ -590,7 +589,6 @@ func TestAddFile(t *testing.T) { for _, c := range errcases { fname := mustWriteFile(t, c.contents) - defer os.Remove(fname) resolver := NewResolver(allUsersExist) _, err := resolver.AddAliasesFile("dom", fname) @@ -637,7 +635,6 @@ y: z` func TestRichFile(t *testing.T) { fname := mustWriteFile(t, richFileContents) - defer os.Remove(fname) resolver := NewResolver(allUsersExist) resolver.DropChars = "." @@ -694,9 +691,6 @@ func TestManyFiles(t *testing.T) { "xd1": mustWriteFile(t, "a: b@xd2"), "xd2": mustWriteFile(t, "b: |cmd"), } - for _, fname := range files { - defer os.Remove(fname) - } resolver := NewResolver(allUsersExist) for domain, fname := range files { @@ -804,8 +798,7 @@ func TestHook(t *testing.T) { if len(rcpts) != 0 { t.Errorf("expected no recipients, got %v", rcpts) } - execErr := &exec.ExitError{} - if !errors.As(err, &execErr) { + if _, ok := errors.AsType[*exec.ExitError](err); !ok { t.Errorf("expected *exec.ExitError, got %T - %v", err, err) } } @@ -846,8 +839,8 @@ func TestParseForward(t *testing.T) { if addr != c.addr { t.Errorf("case %q: got addr %q, expected %q", c.raw, addr, c.addr) } - if !reflect.DeepEqual(via, c.via) { - t.Errorf("case %q: got via %q, expected %q", c.raw, via, c.via) + if diff := cmp.Diff(c.via, via); diff != "" { + t.Errorf("case %q: via mismatch (-want +got):\n%s", c.raw, diff) } } } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 990e58d..181b50b 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "fmt" "testing" + "testing/synctest" "time" "blitiri.com.ar/go/chasquid/internal/dovecot" @@ -59,43 +60,41 @@ func TestDecodeResponse(t *testing.T) { } func TestAuthenticate(t *testing.T) { - db := userdb.New("/dev/null") - db.AddUser("user", "password") - tr := trace.New("test", "TestAuthenticate") - defer tr.Finish() - - a := NewAuthenticator() - a.Register("domain", WrapNoErrorBackend(db)) - - // Shorten the duration to speed up the test. This should still be long - // enough for it to fail if we don't sleep intentionally. - a.AuthDuration = 20 * time.Millisecond - - // Test the correct case first - check(t, a, "user", "domain", "password", true) - - // Wrong password, but valid user@domain. - ts := time.Now() - if ok, _ := a.Authenticate(tr, "user", "domain", "invalid"); ok { - t.Errorf("invalid password, but authentication succeeded") - } - if time.Since(ts) < a.AuthDuration { - t.Errorf("authentication was too fast (invalid case)") - } + synctest.Test(t, func(t *testing.T) { + db := userdb.New("/dev/null") + db.AddUser("user", "password") + tr := trace.New("test", "TestAuthenticate") + defer tr.Finish() + + a := NewAuthenticator() + a.Register("domain", WrapNoErrorBackend(db)) + + // Test the correct case first + check(t, a, "user", "domain", "password", true) + + // Wrong password, but valid user@domain. + ts := time.Now() + if ok, _ := a.Authenticate(tr, "user", "domain", "invalid"); ok { + t.Errorf("invalid password, but authentication succeeded") + } + if time.Since(ts) < a.AuthDuration { + t.Errorf("authentication was too fast (invalid case)") + } - // Incorrect cases, where the user@domain do not exist. - cases := []struct{ user, domain, password string }{ - {"user", "unknown", "password"}, - {"invalid", "domain", "p"}, - {"invalid", "unknown", "p"}, - {"user", "", "password"}, - {"invalid", "", "p"}, - {"", "domain", "password"}, - {"", "", ""}, - } - for _, c := range cases { - check(t, a, c.user, c.domain, c.password, false) - } + // Incorrect cases, where the user@domain do not exist. + cases := []struct{ user, domain, password string }{ + {"user", "unknown", "password"}, + {"invalid", "domain", "p"}, + {"invalid", "unknown", "p"}, + {"user", "", "password"}, + {"invalid", "", "p"}, + {"", "domain", "password"}, + {"", "", ""}, + } + for _, c := range cases { + check(t, a, c.user, c.domain, c.password, false) + } + }) } func check(t *testing.T, a *Authenticator, user, domain, passwd string, expect bool) { @@ -173,45 +172,43 @@ func (d *TestBE) Reload() error { } func TestMultipleBackends(t *testing.T) { - domain1 := NewTestBE() - domain2 := NewTestBE() - fallback := NewTestBE() - - a := NewAuthenticator() - a.Register("domain1", domain1) - a.Register("domain2", domain2) - a.Fallback = fallback - - // Shorten the duration to speed up the test. This should still be long - // enough for it to fail if we don't sleep intentionally. - a.AuthDuration = 20 * time.Millisecond - - domain1.add("user1", "passwd1") - domain2.add("user2", "passwd2") - fallback.add("user3@fallback", "passwd3") - fallback.add("user4@domain1", "passwd4") - - // Successful tests. - cases := []struct{ user, domain, password string }{ - {"user1", "domain1", "passwd1"}, - {"user2", "domain2", "passwd2"}, - {"user3", "fallback", "passwd3"}, - {"user4", "domain1", "passwd4"}, - } - for _, c := range cases { - check(t, a, c.user, c.domain, c.password, true) - } + synctest.Test(t, func(t *testing.T) { + domain1 := NewTestBE() + domain2 := NewTestBE() + fallback := NewTestBE() + + a := NewAuthenticator() + a.Register("domain1", domain1) + a.Register("domain2", domain2) + a.Fallback = fallback + + domain1.add("user1", "passwd1") + domain2.add("user2", "passwd2") + fallback.add("user3@fallback", "passwd3") + fallback.add("user4@domain1", "passwd4") + + // Successful tests. + cases := []struct{ user, domain, password string }{ + {"user1", "domain1", "passwd1"}, + {"user2", "domain2", "passwd2"}, + {"user3", "fallback", "passwd3"}, + {"user4", "domain1", "passwd4"}, + } + for _, c := range cases { + check(t, a, c.user, c.domain, c.password, true) + } - // Unsuccessful tests (users don't exist). - cases = []struct{ user, domain, password string }{ - {"nobody", "domain1", "p"}, - {"nobody", "domain2", "p"}, - {"nobody", "fallback", "p"}, - {"user3", "", "p"}, - } - for _, c := range cases { - check(t, a, c.user, c.domain, c.password, false) - } + // Unsuccessful tests (users don't exist). + cases = []struct{ user, domain, password string }{ + {"nobody", "domain1", "p"}, + {"nobody", "domain2", "p"}, + {"nobody", "fallback", "p"}, + {"user3", "", "p"}, + } + for _, c := range cases { + check(t, a, c.user, c.domain, c.password, false) + } + }) } func TestErrors(t *testing.T) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d46f6de..945751b 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -5,22 +5,21 @@ import ( "os" "testing" - "blitiri.com.ar/go/chasquid/internal/testlib" "blitiri.com.ar/go/log" "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" ) -func mustCreateConfig(t *testing.T, contents string) (string, string) { - tmpDir := testlib.MustTempDir(t) - confStr := []byte(contents) - err := os.WriteFile(tmpDir+"/chasquid.conf", confStr, 0600) +func mustCreateConfig(t *testing.T, contents string) string { + t.Helper() + path := t.ArtifactDir() + "/chasquid.conf" + err := os.WriteFile(path, []byte(contents), 0600) if err != nil { t.Fatalf("Failed to write tmp config: %v", err) } - return tmpDir, tmpDir + "/chasquid.conf" + return path } func TestEmptyStruct(t *testing.T) { @@ -28,8 +27,7 @@ func TestEmptyStruct(t *testing.T) { } func TestEmptyConfig(t *testing.T) { - tmpDir, path := mustCreateConfig(t, "") - defer testlib.RemoveIfOk(t, tmpDir) + path := mustCreateConfig(t, "") c, err := Load(path, "") if err != nil { t.Fatalf("error loading empty config: %v", err) @@ -61,8 +59,7 @@ func TestFullConfig(t *testing.T) { max_queue_items: 345 ` - tmpDir, path := mustCreateConfig(t, confStr) - defer testlib.RemoveIfOk(t, tmpDir) + path := mustCreateConfig(t, confStr) overrideStr := ` hostname: "proust" @@ -118,9 +115,8 @@ func TestErrorLoading(t *testing.T) { } func TestBrokenConfig(t *testing.T) { - tmpDir, path := mustCreateConfig( + path := mustCreateConfig( t, "<invalid> this is not a valid protobuf") - defer testlib.RemoveIfOk(t, tmpDir) c, err := Load(path, "") if err == nil { @@ -129,9 +125,8 @@ func TestBrokenConfig(t *testing.T) { } func TestBrokenOverride(t *testing.T) { - tmpDir, path := mustCreateConfig( + path := mustCreateConfig( t, `hostname: "test"`) - defer testlib.RemoveIfOk(t, tmpDir) c, err := Load(path, "broken override") if err == nil { @@ -140,9 +135,8 @@ func TestBrokenOverride(t *testing.T) { } func TestInvalidGiveUpSendingAfter(t *testing.T) { - tmpDir, path := mustCreateConfig( + path := mustCreateConfig( t, `give_up_send_after: "10"`) - defer testlib.RemoveIfOk(t, tmpDir) c, err := Load(path, "") if err == nil { diff --git a/internal/courier/fakeserver_test.go b/internal/courier/fakeserver_test.go index a183cd3..753f276 100644 --- a/internal/courier/fakeserver_test.go +++ b/internal/courier/fakeserver_test.go @@ -27,7 +27,7 @@ type FakeServer struct { func newFakeServer(t *testing.T, responses map[string]string, conns int) *FakeServer { s := &FakeServer{ t: t, - tmpDir: testlib.MustTempDir(t), + tmpDir: t.ArtifactDir(), responses: responses, conns: conns, wg: &sync.WaitGroup{}, @@ -36,14 +36,6 @@ func newFakeServer(t *testing.T, responses map[string]string, conns int) *FakeSe return s } -func (s *FakeServer) Cleanup() { - // Remove our temporary data. Be extra paranoid and make sure the - // directory isn't too shallow. - if len(s.tmpDir) > 8 { - os.RemoveAll(s.tmpDir) - } -} - func (s *FakeServer) initTLS() { var err error s.tlsConfig, err = testlib.GenerateCert(s.tmpDir) @@ -84,11 +76,7 @@ func (s *FakeServer) start() string { s.initTLS() - s.wg.Add(s.conns) - accept := func() { - defer s.wg.Done() - c, err := l.Accept() if err != nil { panic(err) @@ -137,8 +125,8 @@ func (s *FakeServer) start() string { } } - for i := 0; i < s.conns; i++ { - go accept() + for range s.conns { + s.wg.Go(accept) } return s.addr diff --git a/internal/courier/mda_test.go b/internal/courier/mda_test.go index ef602bf..7b21c64 100644 --- a/internal/courier/mda_test.go +++ b/internal/courier/mda_test.go @@ -7,13 +7,10 @@ import ( "os" "testing" "time" - - "blitiri.com.ar/go/chasquid/internal/testlib" ) func TestMDA(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() p := MDA{ Binary: "tee", diff --git a/internal/courier/smtp_test.go b/internal/courier/smtp_test.go index dd1bf97..fc4cb52 100644 --- a/internal/courier/smtp_test.go +++ b/internal/courier/smtp_test.go @@ -9,7 +9,6 @@ import ( "blitiri.com.ar/go/chasquid/internal/domaininfo" "blitiri.com.ar/go/chasquid/internal/sts" - "blitiri.com.ar/go/chasquid/internal/testlib" "blitiri.com.ar/go/chasquid/internal/trace" ) @@ -27,14 +26,13 @@ func init() { } } -func newSMTP(t *testing.T) (*SMTP, string) { - dir := testlib.MustTempDir(t) - dinfo, err := domaininfo.New(dir) +func newSMTP(t *testing.T) *SMTP { + dinfo, err := domaininfo.New(t.ArtifactDir()) if err != nil { t.Fatal(err) } - return &SMTP{"hello", dinfo, nil}, dir + return &SMTP{"hello", dinfo, nil} } func TestSMTP(t *testing.T) { @@ -52,7 +50,6 @@ func TestSMTP(t *testing.T) { "QUIT": "250 quit ok\n", } srv := newFakeServer(t, responses, 1) - defer srv.Cleanup() host, port := srv.HostPort() // Put a non-existing host first, so we check that if the first host @@ -67,8 +64,7 @@ func TestSMTP(t *testing.T) { } *smtpPort = port - s, tmpDir := newSMTP(t) - defer testlib.RemoveIfOk(t, tmpDir) + s := newSMTP(t) err, _ := s.Deliver("me@me", "to@to", []byte("data")) if err != nil { t.Errorf("deliver failed: %v", err) @@ -125,14 +121,12 @@ func TestSMTPErrors(t *testing.T) { for _, rs := range responses { srv := newFakeServer(t, rs, 1) - defer srv.Cleanup() host, port := srv.HostPort() testMX["to"] = []*net.MX{{Host: host, Pref: 10}} *smtpPort = port - s, tmpDir := newSMTP(t) - defer testlib.RemoveIfOk(t, tmpDir) + s := newSMTP(t) err, _ := s.Deliver("me@me", "to@to", []byte("data")) if err == nil { t.Errorf("deliver not failed in case %q: %v", rs["_welcome"], err) @@ -146,8 +140,7 @@ func TestSMTPErrors(t *testing.T) { func TestNoMXServer(t *testing.T) { testMX["to"] = []*net.MX{} - s, tmpDir := newSMTP(t) - defer testlib.RemoveIfOk(t, tmpDir) + s := newSMTP(t) err, permanent := s.Deliver("me@me", "to@to", []byte("data")) if err == nil { t.Errorf("delivery worked, expected failure") @@ -258,15 +251,13 @@ var tlsResponses = map[string]string{ func TestTLS(t *testing.T) { smtpTotalTimeout = 5 * time.Second srv := newFakeServer(t, tlsResponses, 1) - defer srv.Cleanup() _, *smtpPort = srv.HostPort() testMX["to"] = []*net.MX{ {Host: "localhost", Pref: 20}, } - s, tmpDir := newSMTP(t) - defer testlib.RemoveIfOk(t, tmpDir) + s := newSMTP(t) err, _ := s.Deliver("me@me", "to@to", []byte("data")) if err != nil { t.Errorf("deliver failed: %v", err) @@ -286,7 +277,6 @@ func TestTLS(t *testing.T) { "QUIT": "250 quit ok\n", } srv = newFakeServer(t, responses, 1) - defer srv.Cleanup() _, *smtpPort = srv.HostPort() err, permanent := s.Deliver("me@me", "to@to", []byte("data")) @@ -326,15 +316,13 @@ func TestTLSError(t *testing.T) { // after the failed STARTTLS). Note this also checks that we correctly // close the errored connection, instead of leaving it lingering. srv := newFakeServer(t, responses, 2) - defer srv.Cleanup() _, *smtpPort = srv.HostPort() testMX["to"] = []*net.MX{ {Host: "localhost", Pref: 20}, } - s, tmpDir := newSMTP(t) - defer testlib.RemoveIfOk(t, tmpDir) + s := newSMTP(t) err, _ := s.Deliver("me@me", "to@to", []byte("data")) if err != nil { t.Errorf("deliver failed: %v", err) @@ -353,11 +341,9 @@ func TestTLSError(t *testing.T) { func TestSTSPolicyEnforcement(t *testing.T) { smtpTotalTimeout = 5 * time.Second srv := newFakeServer(t, tlsResponses, 1) - defer srv.Cleanup() _, *smtpPort = srv.HostPort() - s, tmpDir := newSMTP(t) - defer testlib.RemoveIfOk(t, tmpDir) + s := newSMTP(t) a := &attempt{ courier: s, @@ -393,7 +379,6 @@ func TestSTSPolicyEnforcement(t *testing.T) { // be TLS_SECURE which is required by the STS policy. srv = newFakeServer(t, tlsResponses, 1) _, *smtpPort = srv.HostPort() - defer srv.Cleanup() certRoots = srv.rootCA() defer func() { diff --git a/internal/dkim/context_test.go b/internal/dkim/context_test.go index cfaba7d..7d6af34 100644 --- a/internal/dkim/context_test.go +++ b/internal/dkim/context_test.go @@ -10,7 +10,7 @@ import ( func TestTraceNoCtx(t *testing.T) { // Call trace() on a context without a trace function, to check it doesn't // panic. - ctx := context.Background() + ctx := t.Context() trace(ctx, "test") } @@ -19,7 +19,7 @@ func TestTrace(t *testing.T) { traceF := func(f string, a ...any) { s = fmt.Sprintf(f, a...) } - ctx := WithTraceFunc(context.Background(), traceF) + ctx := WithTraceFunc(t.Context(), traceF) trace(ctx, "test %d", 1) if s != "test 1" { t.Errorf("trace function not called") @@ -32,7 +32,7 @@ func TestLookupTXTNoCtx(t *testing.T) { // We just check there is a reasonable error. // We don't specifically check that it's NXDOMAIN because if we don't have // internet access, the error may be different. - ctx := context.Background() + ctx := t.Context() _, err := lookupTXT(ctx, "does.not.exist.example.com") if _, ok := err.(*net.DNSError); !ok { t.Fatalf("expected *net.DNSError, got %T", err) @@ -45,7 +45,7 @@ func TestLookupTXT(t *testing.T) { called = true return nil, nil } - ctx := WithLookupTXTFunc(context.Background(), lookupTXTF) + ctx := WithLookupTXTFunc(t.Context(), lookupTXTF) lookupTXT(ctx, "example.com") if !called { t.Errorf("lookupTXT function not called") @@ -54,7 +54,7 @@ func TestLookupTXT(t *testing.T) { func TestMaxHeaders(t *testing.T) { // First without an override, check we return the default. - ctx := context.Background() + ctx := t.Context() if m := maxHeaders(ctx); m != 5 { t.Errorf("expected 5, got %d", m) } diff --git a/internal/dkim/dns_test.go b/internal/dkim/dns_test.go index 2f7784d..549023c 100644 --- a/internal/dkim/dns_test.go +++ b/internal/dkim/dns_test.go @@ -16,7 +16,7 @@ func TestLookupError(t *testing.T) { errLookupF := func(ctx context.Context, name string) ([]string, error) { return nil, testErr } - ctx := WithLookupTXTFunc(context.Background(), errLookupF) + ctx := WithLookupTXTFunc(t.Context(), errLookupF) pks, err := findPublicKeys(ctx, "example.com", "selector") if pks != nil || err != testErr { @@ -50,7 +50,7 @@ func testLookupTXT(ctx context.Context, name string) ([]string, error) { } func TestSkipBadRecords(t *testing.T) { - ctx := WithLookupTXTFunc(context.Background(), testLookupTXT) + ctx := WithLookupTXTFunc(t.Context(), testLookupTXT) results["selector._domainkey.example.com"] = []string{ "not a tag", "v=DKIM1; p=" + exampleRSAKeyB64, diff --git a/internal/dkim/file_test.go b/internal/dkim/file_test.go index 1a248e8..443f26d 100644 --- a/internal/dkim/file_test.go +++ b/internal/dkim/file_test.go @@ -44,7 +44,7 @@ func TestFromPrivateFiles(t *testing.T) { } func testOne(t *testing.T, base string) { - ctx := context.Background() + ctx := t.Context() ctx = WithTraceFunc(ctx, t.Logf) ctx = loadDNS(t, ctx, base+".dns") diff --git a/internal/dkim/sign_test.go b/internal/dkim/sign_test.go index 641e043..07313e1 100644 --- a/internal/dkim/sign_test.go +++ b/internal/dkim/sign_test.go @@ -1,7 +1,6 @@ package dkim import ( - "context" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" @@ -33,7 +32,7 @@ Joe. `) func TestSignRSA(t *testing.T) { - ctx := context.Background() + ctx := t.Context() ctx = WithTraceFunc(ctx, t.Logf) // Generate a new key pair. @@ -84,7 +83,7 @@ func TestSignRSA(t *testing.T) { } func TestSignEd25519(t *testing.T) { - ctx := context.Background() + ctx := t.Context() ctx = WithTraceFunc(ctx, t.Logf) // Generate a new key pair. @@ -141,7 +140,7 @@ func TestSignBadMessage(t *testing.T) { Domain: "example.com", Selector: "test", } - _, err := s.Sign(context.Background(), "Bad message") + _, err := s.Sign(t.Context(), "Bad message") if err == nil { t.Errorf("Sign: wanted error; got nil") } @@ -159,7 +158,7 @@ func TestSignBadAlgorithm(t *testing.T) { } s.Signer = priv - _, err = s.Sign(context.Background(), basicMessage) + _, err = s.Sign(t.Context(), basicMessage) if !errors.Is(err, errUnsupportedKeyType) { t.Errorf("Sign: wanted unsupported key type; got %v", err) } diff --git a/internal/dkim/verify_test.go b/internal/dkim/verify_test.go index b9e0710..9729eca 100644 --- a/internal/dkim/verify_test.go +++ b/internal/dkim/verify_test.go @@ -20,7 +20,7 @@ func makeLookupTXT(results map[string][]string) lookupTXTFunc { } func TestVerifyRF6376CExample(t *testing.T) { - ctx := context.Background() + ctx := t.Context() ctx = WithTraceFunc(ctx, t.Logf) // Use the public key from the example in RFC 6376 appendix C. @@ -88,7 +88,7 @@ Joe. } func TestVerifyRFC8463Example(t *testing.T) { - ctx := context.Background() + ctx := t.Context() ctx = WithTraceFunc(ctx, t.Logf) // Use the public keys from the example in RFC 8463 appendix A.2. diff --git a/internal/domaininfo/domaininfo_test.go b/internal/domaininfo/domaininfo_test.go index 7ace0e5..d3ac329 100644 --- a/internal/domaininfo/domaininfo_test.go +++ b/internal/domaininfo/domaininfo_test.go @@ -10,8 +10,7 @@ import ( ) func TestBasic(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() db, err := New(dir) if err != nil { t.Fatal(err) @@ -70,8 +69,7 @@ func TestBasic(t *testing.T) { } func TestNewDomain(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() db, err := New(dir) if err != nil { t.Fatal(err) @@ -101,8 +99,7 @@ func TestNewDomain(t *testing.T) { } func TestProgressions(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() db, err := New(dir) if err != nil { t.Fatal(err) @@ -146,8 +143,7 @@ func TestErrors(t *testing.T) { } // Corrupt/invalid file. - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() db, err := New(dir) if err != nil { t.Fatal(err) @@ -175,8 +171,7 @@ func TestErrors(t *testing.T) { } func TestDirectoryErrors(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() db, err := New(dir + "/db") if err != nil { t.Fatal(err) diff --git a/internal/dovecot/dovecot_test.go b/internal/dovecot/dovecot_test.go index d57a80c..cdb5590 100644 --- a/internal/dovecot/dovecot_test.go +++ b/internal/dovecot/dovecot_test.go @@ -8,8 +8,6 @@ package dovecot import ( "net" "testing" - - "blitiri.com.ar/go/chasquid/internal/testlib" ) func TestUsernameNotSafe(t *testing.T) { @@ -59,8 +57,7 @@ func TestAutodetect(t *testing.T) { } // Create a temporary directory, and two sockets on it. - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.TempDir() userdb := dir + "/userdb" client := dir + "/client" diff --git a/internal/localrpc/client_test.go b/internal/localrpc/client_test.go index 44d55a3..6991a24 100644 --- a/internal/localrpc/client_test.go +++ b/internal/localrpc/client_test.go @@ -6,22 +6,29 @@ import ( "io/fs" "net" "net/textproto" - "os" "path/filepath" "testing" ) +// NewFakeServer starts a fake server listening on the given path, which +// replies with the given output to every request. func NewFakeServer(t *testing.T, path, output string) { t.Helper() lis, err := net.Listen("unix", path) if err != nil { - panic(err) + t.Fatal(err) } + t.Cleanup(func() { lis.Close() }) + + go fakeServe(t, lis, output) +} +func fakeServe(t *testing.T, lis net.Listener, output string) { for { conn, err := lis.Accept() if err != nil { - panic(err) + // Listener closed at the end of the test. + return } t.Logf("FakeServer %v: accepted ", conn) @@ -39,20 +46,14 @@ func NewFakeServer(t *testing.T, path, output string) { } func TestBadServer(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "rpc-test-*") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - socketPath := filepath.Join(tmpDir, "rpc.sock") + socketPath := filepath.Join(t.TempDir(), "rpc.sock") // textproto client expects a numeric code, this should cause ReadCodeLine // to fail with textproto.ProtocolError. - go NewFakeServer(t, socketPath, "xxx") - waitForServer(t, socketPath) + NewFakeServer(t, socketPath, "xxx") client := NewClient(socketPath) - _, err = client.Call("Echo") + _, err := client.Call("Echo") if err == nil { t.Fatal("expected error") } diff --git a/internal/localrpc/e2e_test.go b/internal/localrpc/e2e_test.go index ad9744f..82f18fb 100644 --- a/internal/localrpc/e2e_test.go +++ b/internal/localrpc/e2e_test.go @@ -4,9 +4,7 @@ import ( "errors" "net" "net/url" - "os" "testing" - "time" "blitiri.com.ar/go/chasquid/internal/trace" "github.com/google/go-cmp/cmp" @@ -29,7 +27,6 @@ func HolaErr(tr *trace.Trace, input url.Values) (url.Values, error) { } type testServer struct { - dir string sock string *Server } @@ -37,29 +34,23 @@ type testServer struct { func newTestServer(t *testing.T) *testServer { t.Helper() - tmpDir, err := os.MkdirTemp("", "rpc-test-*") - if err != nil { - t.Fatal(err) - } - tsrv := &testServer{ - dir: tmpDir, - sock: tmpDir + "/sock", + sock: t.TempDir() + "/sock", Server: NewServer(), } tsrv.Register("Echo", Echo) tsrv.Register("Hola", Hola) tsrv.Register("HolaErr", HolaErr) - go tsrv.ListenAndServe(tsrv.sock) - waitForServer(t, tsrv.sock) - return tsrv -} + lis, err := net.Listen("unix", tsrv.sock) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { lis.Close() }) + go tsrv.Serve(lis) -func (tsrv *testServer) Cleanup() { - tsrv.Close() - os.RemoveAll(tsrv.dir) + return tsrv } func mkV(args ...string) url.Values { @@ -72,7 +63,6 @@ func mkV(args ...string) url.Values { func TestEndToEnd(t *testing.T) { srv := newTestServer(t) - defer srv.Cleanup() // Run the client. client := NewClient(srv.sock) @@ -112,21 +102,6 @@ func TestEndToEnd(t *testing.T) { } } -func waitForServer(t *testing.T, path string) { - t.Helper() - for range 100 { - time.Sleep(10 * time.Millisecond) - conn, err := net.Dial("unix", path) - if conn != nil { - conn.Close() - } - if err == nil { - return - } - } - t.Fatal("server didn't start") -} - // Allow us to compare errors with cmp.Diff by their string content (since the // instances/types don't carry across RPC boundaries). var transformErrors = cmp.Transformer( diff --git a/internal/localrpc/localrpc.go b/internal/localrpc/localrpc.go index 2675408..6bd74fe 100644 --- a/internal/localrpc/localrpc.go +++ b/internal/localrpc/localrpc.go @@ -47,19 +47,24 @@ func (s *Server) Register(name string, handler Handler) { // ListenAndServe starts the server. func (s *Server) ListenAndServe(path string) error { - tr := trace.New("LocalRPC.Server", path) - defer tr.Finish() - // Previous instances of the server may have shut down uncleanly, leaving // behind the socket file. Remove it just in case. os.Remove(path) - var err error - s.lis, err = net.Listen("unix", path) + lis, err := net.Listen("unix", path) if err != nil { return err } + return s.Serve(lis) +} + +// Serve connections from the given listener. +func (s *Server) Serve(lis net.Listener) error { + tr := trace.New("LocalRPC.Server", lis.Addr().String()) + defer tr.Finish() + + s.lis = lis tr.Printf("Listening") for { conn, err := s.lis.Accept() diff --git a/internal/nettrace/bench_test.go b/internal/nettrace/bench_test.go index 9fd1f81..c2fb3b2 100644 --- a/internal/nettrace/bench_test.go +++ b/internal/nettrace/bench_test.go @@ -39,7 +39,7 @@ func BenchmarkTrace_10000(b *testing.B) { } func BenchmarkNewAndFinish(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { tr := New("bench", "test") tr.Finish() } @@ -48,8 +48,7 @@ func BenchmarkNewAndFinish(b *testing.B) { func BenchmarkPrintf(b *testing.B) { tr := New("bench", "test") defer tr.Finish() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { // Keep this without any formatting, so we measure our code instead of // the performance of fmt.Sprintf. tr.Printf("this is printf") diff --git a/internal/nettrace/context_test.go b/internal/nettrace/context_test.go index 11eb401..bd14775 100644 --- a/internal/nettrace/context_test.go +++ b/internal/nettrace/context_test.go @@ -1,7 +1,6 @@ package nettrace import ( - "context" "testing" ) @@ -10,7 +9,7 @@ func TestContext(t *testing.T) { defer tr.Finish() // Attach the trace to a new context. - ctx := NewContext(context.Background(), tr) + ctx := NewContext(t.Context(), tr) // Get the trace back from the context. { @@ -46,7 +45,7 @@ func TestContext(t *testing.T) { // FromContextOrNew needs to create a new one. { tr5, ctx5 := FromContextOrNew( - context.Background(), "TestContext", "tr5") + t.Context(), "TestContext", "tr5") if tr, _ := FromContext(ctx5); tr != tr5 { t.Errorf("Context with trace returned the wrong trace: %v != %v", tr, tr5) @@ -57,7 +56,7 @@ func TestContext(t *testing.T) { // Child from a context that has no trace attached. { tr6 := ChildFromContext( - context.Background(), "TestContext", "child") + t.Context(), "TestContext", "child") tr6.Finish() if p := tr6.(*trace).Parent; p != nil { t.Errorf("Expected orphan trace, it has a parent: %v", p) diff --git a/internal/normalize/normalize_test.go b/internal/normalize/normalize_test.go index 31fa0f2..623bd98 100644 --- a/internal/normalize/normalize_test.go +++ b/internal/normalize/normalize_test.go @@ -197,8 +197,7 @@ func BenchmarkToCRLF(b *testing.B) { } buf := bb.Bytes() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { ToCRLF(buf) } } @@ -211,8 +210,7 @@ func BenchmarkStringToCRLF(b *testing.B) { } s := sb.String() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { StringToCRLF(s) } } diff --git a/internal/protoio/protoio_test.go b/internal/protoio/protoio_test.go index cc58d35..5dc2829 100644 --- a/internal/protoio/protoio_test.go +++ b/internal/protoio/protoio_test.go @@ -5,20 +5,18 @@ import ( "testing" "blitiri.com.ar/go/chasquid/internal/protoio/testpb" - "blitiri.com.ar/go/chasquid/internal/testlib" ) func TestBin(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + fname := t.ArtifactDir() + "/f" pb := &testpb.M{Content: "hola"} - if err := WriteMessage("f", pb, 0600); err != nil { + if err := WriteMessage(fname, pb, 0600); err != nil { t.Error(err) } pb2 := &testpb.M{} - if err := ReadMessage("f", pb2); err != nil { + if err := ReadMessage(fname, pb2); err != nil { t.Error(err) } if pb.Content != pb2.Content { @@ -27,16 +25,15 @@ func TestBin(t *testing.T) { } func TestText(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + fname := t.ArtifactDir() + "/f" pb := &testpb.M{Content: "hola"} - if err := WriteTextMessage("f", pb, 0600); err != nil { + if err := WriteTextMessage(fname, pb, 0600); err != nil { t.Error(err) } pb2 := &testpb.M{} - if err := ReadTextMessage("f", pb2); err != nil { + if err := ReadTextMessage(fname, pb2); err != nil { t.Error(err) } if pb.Content != pb2.Content { @@ -45,8 +42,7 @@ func TestText(t *testing.T) { } func TestStore(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() st, err := NewStore(dir + "/store") if err != nil { t.Fatalf("failed to create store: %v", err) @@ -98,8 +94,6 @@ func mustCreate(t *testing.T, fname string) { } func TestFileErrors(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) pb := &testpb.M{Content: "hola"} if err := WriteMessage("/proc/doesnotexist", pb, 0600); err == nil { @@ -125,18 +119,17 @@ func TestFileErrors(t *testing.T) { } func TestMarshalErrors(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() // The marshaller enforces that strings are well-formed utf8. So to create // a marshalling error, we use a non-utf8 string. pb := &testpb.M{Content: "\xc3\x28"} - if err := WriteMessage("f", pb, 0600); err == nil { + if err := WriteMessage(dir+"/f", pb, 0600); err == nil { t.Errorf("write worked, expected error") } - if err := WriteTextMessage("ft", pb, 0600); err == nil { + if err := WriteTextMessage(dir+"/ft", pb, 0600); err == nil { t.Errorf("text write worked, expected error") } } diff --git a/internal/queue/queue_test.go b/internal/queue/queue_test.go index ee587c7..4ca3870 100644 --- a/internal/queue/queue_test.go +++ b/internal/queue/queue_test.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" "testing" + "testing/synctest" "time" "blitiri.com.ar/go/chasquid/internal/aliases" @@ -18,224 +19,240 @@ func allUsersExist(tr *trace.Trace, user, domain string) (bool, error) { } func TestBasic(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) - localC := testlib.NewTestCourier() - remoteC := testlib.NewTestCourier() - q, _ := New(dir, set.NewString("loco"), - aliases.NewResolver(allUsersExist), - localC, remoteC) - tr := trace.New("test", "TestBasic") - defer tr.Finish() - - localC.Expect(2) - remoteC.Expect(1) - id, err := q.Put(tr, "from", []string{"am@loco", "x@remote", "nodomain"}, []byte("data")) - if err != nil { - t.Fatalf("Put: %v", err) - } + synctest.Test(t, func(t *testing.T) { + dir := t.ArtifactDir() + localC := testlib.NewTestCourier() + remoteC := testlib.NewTestCourier() + q, _ := New(dir, set.NewString("loco"), + aliases.NewResolver(allUsersExist), + localC, remoteC) + tr := trace.New("test", "TestBasic") + defer tr.Finish() + + localC.Expect(2) + remoteC.Expect(1) + id, err := q.Put(tr, "from", []string{"am@loco", "x@remote", "nodomain"}, []byte("data")) + if err != nil { + t.Fatalf("Put: %v", err) + } - if len(id) < 6 { - t.Errorf("short ID: %v", id) - } + if len(id) < 6 { + t.Errorf("short ID: %v", id) + } - localC.Wait() - remoteC.Wait() + localC.Wait() + remoteC.Wait() - // Make sure the delivered items leave the queue. - testlib.WaitFor(func() bool { return q.Len() == 0 }, 2*time.Second) - if q.Len() != 0 { - t.Fatalf("%d items not removed from the queue after delivery", q.Len()) - } + // Wait for the send loop to finish, and make sure the delivered items + // leave the queue. + synctest.Wait() + if q.Len() != 0 { + t.Fatalf("%d items not removed from the queue after delivery", q.Len()) + } - cases := []struct { - courier *testlib.TestCourier - expectedTo string - }{ - {localC, "nodomain"}, - {localC, "am@loco"}, - {remoteC, "x@remote"}, - } - for _, c := range cases { - req := c.courier.ReqFor[c.expectedTo] - if req == nil { - t.Errorf("missing request for %q", c.expectedTo) - continue + cases := []struct { + courier *testlib.TestCourier + expectedTo string + }{ + {localC, "nodomain"}, + {localC, "am@loco"}, + {remoteC, "x@remote"}, } + for _, c := range cases { + req := c.courier.ReqFor[c.expectedTo] + if req == nil { + t.Errorf("missing request for %q", c.expectedTo) + continue + } - if req.From != "from" || req.To != c.expectedTo || - !bytes.Equal(req.Data, []byte("data")) { - t.Errorf("wrong request for %q: %v", c.expectedTo, req) + if req.From != "from" || req.To != c.expectedTo || + !bytes.Equal(req.Data, []byte("data")) { + t.Errorf("wrong request for %q: %v", c.expectedTo, req) + } } - } + }) } func TestDSNOnTimeout(t *testing.T) { - localC := testlib.NewTestCourier() - remoteC := testlib.NewTestCourier() - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) - q, _ := New(dir, set.NewString("loco"), - aliases.NewResolver(allUsersExist), - localC, remoteC) + synctest.Test(t, func(t *testing.T) { + localC := testlib.NewTestCourier() + remoteC := testlib.NewTestCourier() + dir := t.ArtifactDir() + q, _ := New(dir, set.NewString("loco"), + aliases.NewResolver(allUsersExist), + localC, remoteC) + + // Insert an expired item in the queue. + item := &Item{ + Message: Message{ + ID: <-newID, + From: "from@loco", + Rcpt: []*Recipient{ + mkR("to@to", Recipient_EMAIL, Recipient_PENDING, "err", "to@to")}, + Data: []byte("data"), + }, + CreatedAt: time.Now().Add(-24 * time.Hour), + } + q.q[item.ID] = item + err := item.WriteTo(q.path) + if err != nil { + t.Errorf("failed to write item: %v", err) + } - // Insert an expired item in the queue. - item := &Item{ - Message: Message{ - ID: <-newID, - From: "from@loco", - Rcpt: []*Recipient{ - mkR("to@to", Recipient_EMAIL, Recipient_PENDING, "err", "to@to")}, - Data: []byte("data"), - }, - CreatedAt: time.Now().Add(-24 * time.Hour), - } - q.q[item.ID] = item - err := item.WriteTo(q.path) - if err != nil { - t.Errorf("failed to write item: %v", err) - } + // Exercise DumpString while at it. + q.DumpString() - // Exercise DumpString while at it. - q.DumpString() + // Launch the sending loop, expect 1 local delivery (the DSN). + localC.Expect(1) + go item.SendLoop(q) + localC.Wait() - // Launch the sending loop, expect 1 local delivery (the DSN). - localC.Expect(1) - go item.SendLoop(q) - localC.Wait() + req := localC.ReqFor["from@loco"] + if req == nil { + t.Fatal("missing DSN") + } - req := localC.ReqFor["from@loco"] - if req == nil { - t.Fatal("missing DSN") - } + if req.From != "<>" || req.To != "from@loco" || + !strings.Contains(string(req.Data), "X-Failed-Recipients: to@to,") { + t.Errorf("wrong DSN: %q", string(req.Data)) + } - if req.From != "<>" || req.To != "from@loco" || - !strings.Contains(string(req.Data), "X-Failed-Recipients: to@to,") { - t.Errorf("wrong DSN: %q", string(req.Data)) - } + // Wait for the send loops to finish, so they don't race with the test + // directory cleanup. + synctest.Wait() + }) } func TestAliases(t *testing.T) { - localC := testlib.NewTestCourier() - remoteC := testlib.NewTestCourier() - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) - q, _ := New(dir, set.NewString("loco"), - aliases.NewResolver(allUsersExist), - localC, remoteC) - tr := trace.New("test", "TestAliases") - defer tr.Finish() - - q.aliases.AddDomain("loco") - q.aliases.AddAliasForTesting("ab@loco", "pq@loco", nil, aliases.EMAIL) - q.aliases.AddAliasForTesting("ab@loco", "rs@loco", nil, aliases.EMAIL) - q.aliases.AddAliasForTesting("cd@loco", "ata@hualpa", nil, aliases.EMAIL) - q.aliases.AddAliasForTesting( - "fwd@loco", "fwd@loco", []string{"server"}, aliases.FORWARD) - q.aliases.AddAliasForTesting( - "remote@loco", "remote@rana", []string{"server"}, aliases.FORWARD) - // Note the pipe aliases are tested below, as they don't use the couriers - // and it can be quite inconvenient to test them in this way. - - localC.Expect(2) - remoteC.Expect(3) - - // One email from a local domain: from@loco -> ab@loco, cd@loco, fwd@loco. - _, err := q.Put(tr, "from@loco", - []string{"ab@loco", "cd@loco", "fwd@loco"}, - []byte("data")) - if err != nil { - t.Fatalf("Put: %v", err) - } + synctest.Test(t, func(t *testing.T) { + localC := testlib.NewTestCourier() + remoteC := testlib.NewTestCourier() + dir := t.ArtifactDir() + q, _ := New(dir, set.NewString("loco"), + aliases.NewResolver(allUsersExist), + localC, remoteC) + tr := trace.New("test", "TestAliases") + defer tr.Finish() + + q.aliases.AddDomain("loco") + q.aliases.AddAliasForTesting("ab@loco", "pq@loco", nil, aliases.EMAIL) + q.aliases.AddAliasForTesting("ab@loco", "rs@loco", nil, aliases.EMAIL) + q.aliases.AddAliasForTesting("cd@loco", "ata@hualpa", nil, aliases.EMAIL) + q.aliases.AddAliasForTesting( + "fwd@loco", "fwd@loco", []string{"server"}, aliases.FORWARD) + q.aliases.AddAliasForTesting( + "remote@loco", "remote@rana", []string{"server"}, aliases.FORWARD) + // Note the pipe aliases are tested below, as they don't use the couriers + // and it can be quite inconvenient to test them in this way. + + localC.Expect(2) + remoteC.Expect(3) + + // One email from a local domain: from@loco -> ab@loco, cd@loco, fwd@loco. + _, err := q.Put(tr, "from@loco", + []string{"ab@loco", "cd@loco", "fwd@loco"}, + []byte("data")) + if err != nil { + t.Fatalf("Put: %v", err) + } - // And another from a remote domain: from@rana -> remote@loco - _, err = q.Put(tr, "from@rana", - []string{"remote@loco"}, - []byte("data")) - if err != nil { - t.Fatalf("Put: %v", err) - } + // And another from a remote domain: from@rana -> remote@loco + _, err = q.Put(tr, "from@rana", + []string{"remote@loco"}, + []byte("data")) + if err != nil { + t.Fatalf("Put: %v", err) + } - localC.Wait() - remoteC.Wait() - - cases := []struct { - courier *testlib.TestCourier - expectedFrom string - expectedTo string - }{ - // From the local domain: from@loco - {localC, "from@loco", "pq@loco"}, - {localC, "from@loco", "rs@loco"}, - {remoteC, "from@loco", "ata@hualpa"}, - {remoteC, "from@loco", "fwd@loco"}, - - // From the remote domain: from@rana. - // Note the SRS in the remoteC. - {remoteC, "remote+fwd_from=from=rana@loco", "remote@rana"}, - } - for _, c := range cases { - req := c.courier.ReqFor[c.expectedTo] - if req == nil { - t.Errorf("missing request for %q", c.expectedTo) - continue + localC.Wait() + remoteC.Wait() + + cases := []struct { + courier *testlib.TestCourier + expectedFrom string + expectedTo string + }{ + // From the local domain: from@loco + {localC, "from@loco", "pq@loco"}, + {localC, "from@loco", "rs@loco"}, + {remoteC, "from@loco", "ata@hualpa"}, + {remoteC, "from@loco", "fwd@loco"}, + + // From the remote domain: from@rana. + // Note the SRS in the remoteC. + {remoteC, "remote+fwd_from=from=rana@loco", "remote@rana"}, } + for _, c := range cases { + req := c.courier.ReqFor[c.expectedTo] + if req == nil { + t.Errorf("missing request for %q", c.expectedTo) + continue + } - if req.From != c.expectedFrom || req.To != c.expectedTo || - !bytes.Equal(req.Data, []byte("data")) { - t.Errorf("wrong request for %q: %v", c.expectedTo, *req) + if req.From != c.expectedFrom || req.To != c.expectedTo || + !bytes.Equal(req.Data, []byte("data")) { + t.Errorf("wrong request for %q: %v", c.expectedTo, *req) + } } - } + + // Wait for the send loops to finish, so they don't race with the test + // directory cleanup. + synctest.Wait() + }) } func TestFullQueue(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) - q, _ := New(dir, set.NewString(), - aliases.NewResolver(allUsersExist), - testlib.DumbCourier, testlib.DumbCourier) - tr := trace.New("test", "TestFullQueue") - defer tr.Finish() + synctest.Test(t, func(t *testing.T) { + dir := t.ArtifactDir() + q, _ := New(dir, set.NewString(), + aliases.NewResolver(allUsersExist), + testlib.DumbCourier, testlib.DumbCourier) + tr := trace.New("test", "TestFullQueue") + defer tr.Finish() + + // Force-insert as many items in the queue as it supports. + oneID := "" + for i := 0; i < q.MaxItems; i++ { + item := &Item{ + Message: Message{ + ID: <-newID, + From: fmt.Sprintf("from-%d", i), + Rcpt: []*Recipient{ + mkR("to", Recipient_EMAIL, Recipient_PENDING, "", "")}, + Data: []byte("data"), + }, + CreatedAt: time.Now(), + } + q.q[item.ID] = item + oneID = item.ID + } - // Force-insert as many items in the queue as it supports. - oneID := "" - for i := 0; i < q.MaxItems; i++ { - item := &Item{ - Message: Message{ - ID: <-newID, - From: fmt.Sprintf("from-%d", i), - Rcpt: []*Recipient{ - mkR("to", Recipient_EMAIL, Recipient_PENDING, "", "")}, - Data: []byte("data"), - }, - CreatedAt: time.Now(), + // This one should fail due to the queue being too big. + id, err := q.Put(tr, "from", []string{"to"}, []byte("data-qf")) + if err != errQueueFull { + t.Errorf("Not failed as expected: %v - %v", id, err) } - q.q[item.ID] = item - oneID = item.ID - } - // This one should fail due to the queue being too big. - id, err := q.Put(tr, "from", []string{"to"}, []byte("data-qf")) - if err != errQueueFull { - t.Errorf("Not failed as expected: %v - %v", id, err) - } + // Remove one, and try again: it should succeed. + // Write it first so we don't get complaints about the file not existing + // (as we did not all the items properly). + q.q[oneID].WriteTo(q.path) + q.Remove(oneID) - // Remove one, and try again: it should succeed. - // Write it first so we don't get complaints about the file not existing - // (as we did not all the items properly). - q.q[oneID].WriteTo(q.path) - q.Remove(oneID) + id, err = q.Put(tr, "from", []string{"to"}, []byte("data")) + if err != nil { + t.Errorf("Put: %v", err) + } + q.Remove(id) - id, err = q.Put(tr, "from", []string{"to"}, []byte("data")) - if err != nil { - t.Errorf("Put: %v", err) - } - q.Remove(id) + // Wait for the send loops to finish, so they don't race with the test + // directory cleanup. + synctest.Wait() + }) } func TestPipes(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() q, _ := New(dir, set.NewString("loco"), aliases.NewResolver(allUsersExist), testlib.DumbCourier, testlib.DumbCourier) @@ -290,44 +307,49 @@ func TestNextDelay(t *testing.T) { } func TestSerialization(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + synctest.Test(t, func(t *testing.T) { + dir := t.ArtifactDir() - // Save an item in the queue directory. - item := &Item{ - Message: Message{ - ID: <-newID, - From: "from@loco", - Rcpt: []*Recipient{ - mkR("to@to", Recipient_EMAIL, Recipient_PENDING, "err", "to@to")}, - Data: []byte("data"), - }, - CreatedAt: time.Now().Add(-1 * time.Hour), - } - err := item.WriteTo(dir) - if err != nil { - t.Errorf("failed to write item: %v", err) - } + // Save an item in the queue directory. + item := &Item{ + Message: Message{ + ID: <-newID, + From: "from@loco", + Rcpt: []*Recipient{ + mkR("to@to", Recipient_EMAIL, Recipient_PENDING, "err", "to@to")}, + Data: []byte("data"), + }, + CreatedAt: time.Now().Add(-1 * time.Hour), + } + err := item.WriteTo(dir) + if err != nil { + t.Errorf("failed to write item: %v", err) + } - // Create the queue; should load the - remoteC := testlib.NewTestCourier() - remoteC.Expect(1) - q, _ := New(dir, set.NewString("loco"), - aliases.NewResolver(allUsersExist), - testlib.DumbCourier, remoteC) - q.Load() + // Create the queue; should load the + remoteC := testlib.NewTestCourier() + remoteC.Expect(1) + q, _ := New(dir, set.NewString("loco"), + aliases.NewResolver(allUsersExist), + testlib.DumbCourier, remoteC) + q.Load() - // Launch the sending loop, expect 1 remote delivery for the item we saved. - remoteC.Wait() + // Launch the sending loop, expect 1 remote delivery for the item we saved. + remoteC.Wait() - req := remoteC.ReqFor["to@to"] - if req == nil { - t.Fatal("email not delivered") - } + req := remoteC.ReqFor["to@to"] + if req == nil { + t.Fatal("email not delivered") + } - if req.From != "from@loco" || req.To != "to@to" { - t.Errorf("wrong email: %v", req) - } + if req.From != "from@loco" || req.To != "to@to" { + t.Errorf("wrong email: %v", req) + } + + // Wait for the send loops to finish, so they don't race with the test + // directory cleanup. + synctest.Wait() + }) } func mkR(a string, t Recipient_Type, s Recipient_Status, m, o string) *Recipient { diff --git a/internal/safeio/safeio_test.go b/internal/safeio/safeio_test.go index 574a7be..296ca6f 100644 --- a/internal/safeio/safeio_test.go +++ b/internal/safeio/safeio_test.go @@ -42,8 +42,8 @@ func testWriteFile(fname string, data []byte, perm os.FileMode, ops ...FileOp) e } func TestWriteFile(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() + t.Chdir(dir) // Write a new file. content := []byte("content 1") @@ -65,8 +65,8 @@ func TestWriteFile(t *testing.T) { } func TestWriteFileWithOp(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() + t.Chdir(dir) var opFile string op := func(f string) error { @@ -88,8 +88,8 @@ func TestWriteFileWithOp(t *testing.T) { } func TestWriteFileWithFailingOp(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() + t.Chdir(dir) var opFile string opOK := func(f string) error { @@ -166,8 +166,8 @@ func (f *testFile) Close() error { var _ osFile = &testFile{} func TestErrors(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() + t.Chdir(dir) oldCreateTemp := createTemp defer func() { createTemp = oldCreateTemp }() @@ -248,8 +248,8 @@ func checkNotExists(t *testing.T, fname string) { func TestFsyncFileOp(t *testing.T) { // Check it as an operation on a normal file. - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() + t.Chdir(dir) content := []byte("content 1") err := testWriteFile("file1", content, 0660, FsyncFileOp) diff --git a/internal/smtpsrv/conn_test.go b/internal/smtpsrv/conn_test.go index 0453fe2..fad3bb1 100644 --- a/internal/smtpsrv/conn_test.go +++ b/internal/smtpsrv/conn_test.go @@ -2,11 +2,9 @@ package smtpsrv import ( "net" - "os" "testing" "blitiri.com.ar/go/chasquid/internal/domaininfo" - "blitiri.com.ar/go/chasquid/internal/testlib" "blitiri.com.ar/go/chasquid/internal/trace" "blitiri.com.ar/go/spf" ) @@ -14,15 +12,7 @@ import ( func TestSecLevel(t *testing.T) { // We can't simulate this externally because of the SPF record // requirement, so do a narrow test on Conn.secLevelCheck. - // Create the directory by hand because we don't want to automatically - // chdir into it (it affects the fuzzing infrastructure). - dir, err := os.MkdirTemp("", "testlib_") - if err != nil { - t.Fatalf("Failed to create temp dir: %v\n", dir) - } - defer testlib.RemoveIfOk(t, dir) - - dinfo, err := domaininfo.New(dir) + dinfo, err := domaininfo.New(t.ArtifactDir()) if err != nil { t.Fatalf("Failed to create domain info: %v", err) } diff --git a/internal/smtpsrv/server_test.go b/internal/smtpsrv/server_test.go index 298b3d2..13241f0 100644 --- a/internal/smtpsrv/server_test.go +++ b/internal/smtpsrv/server_test.go @@ -12,7 +12,6 @@ import ( "os" "strings" "testing" - "time" "blitiri.com.ar/go/chasquid/internal/aliases" "blitiri.com.ar/go/chasquid/internal/auth" @@ -491,8 +490,7 @@ func TestAddDKIMSigner(t *testing.T) { t.Errorf("AddDKIMSigner: expected not exist, got %v", err) } - tmpDir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, tmpDir) + tmpDir := t.ArtifactDir() // Invalid PEM file. kf1 := tmpDir + "/key1-bad_pem.pem" @@ -555,8 +553,7 @@ func BenchmarkManyEmails(b *testing.B) { c := mustDial(b, ModeSMTP, false) defer c.Close() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { sendEmail(b, c) } } @@ -576,25 +573,6 @@ func BenchmarkManyEmailsParallel(b *testing.B) { // === Test environment === // -// waitForServer waits 5 seconds for the server to start, and returns an error -// if it fails to do so. -// It does this by repeatedly connecting to the address until it either -// replies or times out. Note we do not do any validation of the reply. -func waitForServer(addr string) error { - start := time.Now() - for time.Since(start) < 10*time.Second { - conn, err := net.Dial("tcp", addr) - if err == nil { - conn.Close() - return nil - } - - time.Sleep(100 * time.Millisecond) - } - - return fmt.Errorf("not reachable") -} - type brokenAuthBE struct{} func (b brokenAuthBE) Authenticate(user, password string) (bool, error) { @@ -615,11 +593,7 @@ func realMain(m *testing.M) int { flag.Parse() // Create a 1MiB string, which the large message tests use. - buf := make([]byte, 1024*1024) - for i := range buf { - buf[i] = 'a' - } - str1MiB = string(buf) + str1MiB = strings.Repeat("a", 1024*1024) // Set up the mail log to stdout, which is captured by the test runner, // so we have better debugging information on failures. @@ -647,17 +621,29 @@ func realMain(m *testing.M) int { return 1 } - smtpAddr = testlib.GetFreePort() - submissionAddr = testlib.GetFreePort() - submissionTLSAddr = testlib.GetFreePort() - s := NewServer() s.Hostname = "localhost" s.MaxDataSize = int64(maxDataSizeMiB) * 1024 * 1024 s.AddCerts(tmpDir+"/cert.pem", tmpDir+"/key.pem") - s.AddAddr(smtpAddr, ModeSMTP) - s.AddAddr(submissionAddr, ModeSubmission) - s.AddAddr(submissionTLSAddr, ModeSubmissionTLS) + + // Open the listeners ourselves, so the ports are already bound + // (and we know their addresses) by the time the tests run. + for _, l := range []struct { + addr *string + mode SocketMode + }{ + {&smtpAddr, ModeSMTP}, + {&submissionAddr, ModeSubmission}, + {&submissionTLSAddr, ModeSubmissionTLS}, + } { + ln, err := net.Listen("tcp", "localhost:0") + if err != nil { + fmt.Printf("Failed to listen: %v\n", err) + return 1 + } + *l.addr = ln.Addr().String() + s.AddListeners([]net.Listener{ln}, l.mode) + } s.InitQueue(tmpDir+"/queue", localC, remoteC) @@ -687,9 +673,6 @@ func realMain(m *testing.M) int { go s.ListenAndServe() } - waitForServer(smtpAddr) - waitForServer(submissionAddr) - waitForServer(submissionTLSAddr) return m.Run() } diff --git a/internal/sts/sts_test.go b/internal/sts/sts_test.go index d581da9..f3617b7 100644 --- a/internal/sts/sts_test.go +++ b/internal/sts/sts_test.go @@ -72,7 +72,7 @@ func TestMain(m *testing.M) { httpServer := httptest.NewServer(http.HandlerFunc(testHTTPHandler)) fakeURLForTesting = httpServer.URL - os.Exit(m.Run()) + m.Run() } func TestParsePolicy(t *testing.T) { @@ -196,21 +196,21 @@ func TestFetch(t *testing.T) { // defined in TestMain above. See httpGet for more details. // Normal fetch, all valid. - p, err := Fetch(context.Background(), "domain.com") + p, err := Fetch(t.Context(), "domain.com") if err != nil { t.Errorf("failed to fetch policy: %v", err) } t.Logf("domain.com: %+v", p) // Domain without a policy (HTTP get fails). - p, err = Fetch(context.Background(), "policy404") + p, err = Fetch(t.Context(), "policy404") if err == nil { t.Errorf("fetched unknown policy: %v", p) } t.Logf("policy404: got error as expected: %v", err) // Domain with an invalid policy (unknown version). - p, err = Fetch(context.Background(), "version99") + p, err = Fetch(t.Context(), "version99") if err != ErrUnknownVersion { t.Errorf("expected error %v, got %v (and policy: %v)", ErrUnknownVersion, err, p) @@ -218,7 +218,7 @@ func TestFetch(t *testing.T) { t.Logf("version99: got expected error: %v", err) // Error fetching TXT record for this domain. - p, err = Fetch(context.Background(), "domErr") + p, err = Fetch(t.Context(), "domErr") if err != errTest { t.Errorf("expected error %v, got %v (and policy: %v)", errTest, err, p) @@ -235,7 +235,7 @@ func TestPolicyTooBig(t *testing.T) { raw += `"mxlast"], "max_age": 100}` policyForDomain["toobig"] = raw - _, err := Fetch(context.Background(), "toobig") + _, err := Fetch(t.Context(), "toobig") if err == nil { t.Errorf("fetch worked, but should have failed") } @@ -251,8 +251,7 @@ func expvarMustEq(t *testing.T, name string, v *expvar.Int, expected int64) { } func TestCacheBasics(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() c, err := NewCache(dir) if err != nil { @@ -266,7 +265,7 @@ func TestCacheBasics(t *testing.T) { cacheFetches.Set(0) cacheHits.Set(0) - ctx := context.Background() + ctx := t.Context() // Fetch domain.com, check we get a reasonable policy, and that it's a // cache miss. @@ -314,15 +313,14 @@ func TestCacheBasics(t *testing.T) { // Test how the cache behaves when the files are corrupt. func TestCacheBadData(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() c, err := NewCache(dir) if err != nil { t.Fatal(err) } - ctx := context.Background() + ctx := t.Context() cacheUnmarshalErrors.Set(0) cacheInvalid.Set(0) @@ -391,6 +389,7 @@ func (c *PolicyCache) mustFetch(ctx context.Context, t *testing.T, d string) *Po } func mustRewriteAndChtime(t *testing.T, fname, content string) { + t.Helper() testlib.Rewrite(t, fname, content) // Advance the expiration time to the future, so the rewritten policy is @@ -403,15 +402,14 @@ func mustRewriteAndChtime(t *testing.T, fname, content string) { } func TestCacheRefresh(t *testing.T) { - dir := testlib.MustTempDir(t) - defer testlib.RemoveIfOk(t, dir) + dir := t.ArtifactDir() c, err := NewCache(dir) if err != nil { t.Fatal(err) } - ctx := context.Background() + ctx := t.Context() txtResults["_mta-sts.refresh-test"] = []string{"v=STSv1; id=blah;"} policyForDomain["refresh-test"] = ` @@ -439,7 +437,7 @@ func TestCacheRefresh(t *testing.T) { // Launch background refreshes, and wait for one to complete. cacheRefreshCycles.Set(0) - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second) defer cancel() go c.PeriodicallyRefresh(ctx) for cacheRefreshCycles.Value() == 0 { @@ -453,7 +451,7 @@ func TestCacheRefresh(t *testing.T) { } func TestCacheSlashSafe(t *testing.T) { - dir := testlib.MustTempDir(t) + dir := t.ArtifactDir() c, err := NewCache(dir) if err != nil { t.Fatal(err) @@ -519,7 +517,7 @@ func TestHTTPGet(t *testing.T) { })) defer srv1.Close() - ctx := context.Background() + ctx := t.Context() raw, err := httpGet(ctx, srv1.URL) if err != nil { t.Errorf("GET failed: got %q, %v", raw, err) diff --git a/internal/testlib/testlib.go b/internal/testlib/testlib.go index d218300..508d388 100644 --- a/internal/testlib/testlib.go +++ b/internal/testlib/testlib.go @@ -11,78 +11,18 @@ import ( "math/big" "net" "os" - "strings" "sync" "testing" "time" ) -// MustTempDir creates a temporary directory, or dies trying. -func MustTempDir(t *testing.T) string { - dir, err := os.MkdirTemp("", "testlib_") - if err != nil { - t.Fatal(err) - } - - err = os.Chdir(dir) - if err != nil { - t.Fatal(err) - } - - t.Logf("test directory: %q", dir) - return dir -} - -// RemoveIfOk removes the given directory, but only if we have not failed. We -// want to keep the failed directories for debugging. -func RemoveIfOk(t *testing.T, dir string) { - // Safeguard, to make sure we only remove test directories. - // This should help prevent accidental deletions. - if !strings.Contains(dir, "testlib_") { - panic("invalid/dangerous directory") - } - - if !t.Failed() { - os.RemoveAll(dir) - } -} - -// Rewrite a file with the given contents. -func Rewrite(t *testing.T, path, contents string) error { - // Safeguard, to make sure we only mess with test files. - if !strings.Contains(path, "testlib_") { - panic("invalid/dangerous path") - } - +// Rewrite a file with the given contents, or fail the test. +func Rewrite(t *testing.T, path, contents string) { + t.Helper() err := os.WriteFile(path, []byte(contents), 0600) if err != nil { - t.Errorf("failed to rewrite file: %v", err) - } - - return err -} - -// GetFreePort returns a free TCP port. This is hacky and not race-free, but -// it works well enough for testing purposes. -func GetFreePort() string { - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - panic(err) - } - defer l.Close() - return l.Addr().String() -} - -// WaitFor f to return true (returns true), or d to pass (returns false). -func WaitFor(f func() bool, d time.Duration) bool { - start := time.Now() - for time.Since(start) < d { - if f() { - return true - } - time.Sleep(20 * time.Millisecond) + t.Fatalf("failed to rewrite file: %v", err) } - return false } type deliverRequest struct { @@ -97,27 +37,27 @@ type TestCourier struct { wg sync.WaitGroup Requests []*deliverRequest ReqFor map[string]*deliverRequest - sync.Mutex + mu sync.Mutex } // Deliver the given mail (saving it in tc.Requests). func (tc *TestCourier) Deliver(from string, to string, data []byte) (error, bool) { defer tc.wg.Done() dr := &deliverRequest{from, to, data, nil} - tc.Lock() + tc.mu.Lock() tc.Requests = append(tc.Requests, dr) tc.ReqFor[to] = dr - tc.Unlock() + tc.mu.Unlock() return nil, false } func (tc *TestCourier) Forward(from string, to string, data []byte, servers []string) (error, bool) { defer tc.wg.Done() dr := &deliverRequest{from, to, data, servers} - tc.Lock() + tc.mu.Lock() tc.Requests = append(tc.Requests, dr) tc.ReqFor[to] = dr - tc.Unlock() + tc.mu.Unlock() return nil, false } diff --git a/internal/testlib/testlib_test.go b/internal/testlib/testlib_test.go index 7cbc984..15e9d9a 100644 --- a/internal/testlib/testlib_test.go +++ b/internal/testlib/testlib_test.go @@ -1,107 +1,26 @@ package testlib import ( - "errors" - "io/fs" "os" "testing" - "time" ) -func TestBasic(t *testing.T) { - dir := MustTempDir(t) - if err := os.WriteFile(dir+"/file", nil, 0660); err != nil { - t.Fatalf("could not create file in %s: %v", dir, err) - } - - wd, err := os.Getwd() - if err != nil { - t.Fatalf("could not get working directory: %v", err) - } - if wd != dir { - t.Errorf("MustTempDir did not change directory") - t.Errorf(" expected %q, got %q", dir, wd) - } - - RemoveIfOk(t, dir) - if _, err := os.Stat(dir); !errors.Is(err, fs.ErrNotExist) { - t.Fatalf("%s existed, should have been deleted: %v", dir, err) - } -} - -func TestRemoveCheck(t *testing.T) { - defer func() { - if r := recover(); r != nil { - t.Logf("recovered: %v", r) - } else { - t.Fatalf("check did not panic as expected") - } - }() - - RemoveIfOk(t, "/tmp/something") -} - -func TestLeaveDirOnError(t *testing.T) { - myt := &testing.T{} - dir := MustTempDir(myt) - myt.Errorf("something bad happened") - - RemoveIfOk(myt, dir) - if _, err := os.Stat(dir); errors.Is(err, fs.ErrNotExist) { - t.Fatalf("%s was removed, should have been kept", dir) - } - - // Remove the directory for real this time. - RemoveIfOk(t, dir) -} - -func TestRewriteSafeguard(t *testing.T) { - myt := &testing.T{} - defer func() { - if r := recover(); r != nil { - t.Logf("recovered: %v", r) - } else { - t.Fatalf("check did not panic as expected") - } - }() - - Rewrite(myt, "/something", "test") -} - func TestRewrite(t *testing.T) { - dir := MustTempDir(t) - defer RemoveIfOk(t, dir) - - myt := &testing.T{} - Rewrite(myt, dir+"/file", "hola") - if myt.Failed() { - t.Errorf("basic rewrite failed") - } -} + fname := t.TempDir() + "/file" + Rewrite(t, fname, "hola") + Rewrite(t, fname, "chau") -func TestGetFreePort(t *testing.T) { - p := GetFreePort() - if p == "" { - t.Errorf("failed to get free port") - } -} - -func TestWaitFor(t *testing.T) { - ok := WaitFor(func() bool { return true }, 20*time.Millisecond) - if !ok { - t.Errorf("WaitFor(true) timed out") + data, err := os.ReadFile(fname) + if err != nil { + t.Fatalf("failed to read file: %v", err) } - - ok = WaitFor(func() bool { return false }, 20*time.Millisecond) - if ok { - t.Errorf("WaitFor(false) worked") + if string(data) != "chau" { + t.Errorf("file content: got %q, expected %q", data, "chau") } } func TestGenerateCert(t *testing.T) { - dir := MustTempDir(t) - defer os.RemoveAll(dir) - conf, err := GenerateCert(dir) + conf, err := GenerateCert(t.TempDir()) if err != nil { t.Errorf("GenerateCert returned error: %v", err) } diff --git a/internal/userdb/userdb_test.go b/internal/userdb/userdb_test.go index 478e519..7d6f327 100644 --- a/internal/userdb/userdb_test.go +++ b/internal/userdb/userdb_test.go @@ -1,34 +1,21 @@ package userdb import ( - "fmt" "os" - "reflect" - "strings" "testing" -) - -// Remove the file if the test was successful. Used in defer statements, to -// leave files around for inspection when the tests failed. -func removeIfSuccessful(t *testing.T, fname string) { - // Safeguard, to make sure we only remove test files. - // This should help prevent accidental deletions. - if !strings.Contains(fname, "userdb_test") { - panic("invalid/dangerous directory") - } - if !t.Failed() { - os.Remove(fname) - } -} + "google.golang.org/protobuf/proto" +) // Create a database with the given content on a temporary filename. Return // the filename, or an error if there were errors creating it. func mustCreateDB(t *testing.T, content string) string { - f, err := os.CreateTemp("", "userdb_test") + t.Helper() + f, err := os.CreateTemp(t.ArtifactDir(), "userdb_test") if err != nil { t.Fatal(err) } + defer f.Close() if _, err := f.WriteString(content); err != nil { t.Fatal(err) @@ -39,22 +26,7 @@ func mustCreateDB(t *testing.T, content string) string { } func dbEquals(a, b *DB) bool { - if a.db == nil || b.db == nil { - return a.db == nil && b.db == nil - } - - if len(a.db.Users) != len(b.db.Users) { - return false - } - - for k, av := range a.db.Users { - bv, ok := b.db.Users[k] - if !ok || !reflect.DeepEqual(av, bv) { - return false - } - } - - return true + return proto.Equal(a.db, b.db) } var emptyDB = &DB{ @@ -80,7 +52,6 @@ func TestEmptyLoad(t *testing.T) { func testOneLoad(t *testing.T, desc, content string, fatal bool, fatalErr error) { fname := mustCreateDB(t, content) - defer removeIfSuccessful(t, fname) db, err := Load(fname) if fatal { if err == nil { @@ -109,7 +80,6 @@ func mustLoad(t *testing.T, fname string) *DB { func TestWrite(t *testing.T) { fname := mustCreateDB(t, "") - defer removeIfSuccessful(t, fname) db := mustLoad(t, fname) if err := db.Write(); err != nil { @@ -170,8 +140,7 @@ func TestWrite(t *testing.T) { } func TestNew(t *testing.T) { - fname := fmt.Sprintf("%s/userdb_test-%d", os.TempDir(), os.Getpid()) - defer os.Remove(fname) + fname := t.TempDir() + "/userdb" db1 := New(fname) db1.AddUser("user", "passwd") db1.Write() @@ -188,7 +157,6 @@ func TestNew(t *testing.T) { func TestInvalidUsername(t *testing.T) { fname := mustCreateDB(t, "") - defer removeIfSuccessful(t, fname) db := mustLoad(t, fname) // Names that are invalid. @@ -227,7 +195,6 @@ func plainPassword(p string) *Password { // debugging, but it should be functional for that purpose. func TestPlainScheme(t *testing.T) { fname := mustCreateDB(t, "") - defer removeIfSuccessful(t, fname) db := mustLoad(t, fname) db.db.Users["user"] = plainPassword("pass word") @@ -248,7 +215,6 @@ func TestPlainScheme(t *testing.T) { // Test the denied scheme. func TestDeniedScheme(t *testing.T) { fname := mustCreateDB(t, "") - defer removeIfSuccessful(t, fname) db := mustLoad(t, fname) db.db.Users["user"] = &Password{Scheme: &Password_Denied{}} @@ -266,7 +232,6 @@ func TestDeniedScheme(t *testing.T) { func TestReload(t *testing.T) { content := "users:< key: 'u1' value:< plain:< password: 'pass' >>>" fname := mustCreateDB(t, content) - defer removeIfSuccessful(t, fname) db := mustLoad(t, fname) // Add a valid line to the file. @@ -306,7 +271,6 @@ func TestReload(t *testing.T) { func TestRemoveUser(t *testing.T) { fname := mustCreateDB(t, "") - defer removeIfSuccessful(t, fname) db := mustLoad(t, fname) if ok := db.RemoveUser("unknown"); ok { @@ -332,7 +296,6 @@ func TestRemoveUser(t *testing.T) { func TestExists(t *testing.T) { fname := mustCreateDB(t, "") - defer removeIfSuccessful(t, fname) db := mustLoad(t, fname) if db.Exists("unknown") {