Files
1856433721 chore(lint): clear the lint baseline and fix six empty assertions (#217)
* chore(lint): clear the lint baseline and lint the whole repository

The v1.5.0 tag run failed Lint with 42 findings that had nothing to do
with the release. The chain:

  1. .golangci.yml set issues.new: true, which makes golangci-lint's diff
     processor run `git diff HEAD~`.
  2. On PR and main-push runs golangci-lint-action fetches a patch from
     the GitHub compare API and passes --new-from-patch --new=false, which
     overrides that config. So the setting was dead on every normal run.
  3. On the tag push the compare call returned 404, so the action invoked
     golangci-lint bare and the config took effect for the first time.
  4. actions/checkout defaults to fetch-depth: 1, so HEAD~ does not exist
     and the diff processor failed. golangci-lint treats that as a warning
     and falls back to reporting everything: 42 pre-existing findings.

issues.new also made local linting lie. `make lint` and the pre-commit
hook both run golangci-lint bare, so in a clean checkout they reported
0 issues while 42 stood.

Fix the 42, then drop both filters so every event type behaves the same
and no git history is needed:

  - testifylint (29): assert.Nil(t, err) -> require/assert.NoError,
    assert.Equal(t, "", x) -> assert.Empty, assert.True(t, strings
    .Contains(..)) -> assert.Contains. Error assertions now abort the
    test rather than continue, which is what require-error asks for.
  - noctx (2): gravatar.go and handler.go already built a ctx with a
    timeout and then threw it away via NewRequest + req.WithContext;
    they now call NewRequestWithContext. No behaviour change.
  - staticcheck (6): strings.ReplaceAll x2, fmt.Fprintf, getMD5Hash
    returns its error last (unexported, two call sites), and the
    unexported gravatarBaseUrl/gravatarUrl renamed to ...URL.
  - ST1003 on the exported Gravatar.GravatarUrl field, ST1005 on the two
    capitalised SMTP error strings, gosmopolitan on the deliberate IDN
    test input and noctx on the one-shot generator are suppressed with
    reasons. The two error strings are passed through verbatim by
    ParseSMTPError as LookupError.Message, so they are user-facing text
    consistent with the Err* constants in error.go, not internal Go
    error strings.

Upstream frames new-from-* as a migration aid for a large existing
codebase, and the FAQ notes that --new (this repo's issues.new) is less
safe than --new-from-rev because generated files derail it -- this repo
has a generator. Of nine comparable Go projects sampled, none set
issues.new and six of seven using golangci-lint-action do not set
only-new-issues.

Verified: a bare `golangci-lint run` and a depth-1 clone both report
0 issues; go build, go vet and 67 offline tests pass under -race. The
network-dependent tests are left to CI, as they make real SMTP
connections.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* test: make six vacuous error assertions actually assert

testifylint's require-error check surfaced these while clearing the lint
baseline. All six passed a value that reads as the expected error into
testify's msgAndArgs parameter, which is only used to decorate a failure
message. The expectation was never applied: the assertions checked no
more than err != nil.

Demonstrated on the 404 case -- with the old form, an expected
"status_code: 403" passes against an actual 404.

  mx_test.go:26          assert.Error(t, err, ErrNoSuchHost)
  smtp_test.go:141       assert.Error(t, err, ErrNoSuchHost)
  smtp_test.go:159       assert.Error(t, err, syscall.ECONNREFUSED)
  handler_test.go:186    assert.Error(t, err, "...status_code: 404")
  handler_test.go:196    assert.Error(t, err, "...status_code: 500")
  handler_test.go:219    assert.Error(t, err, "invalid character ...")

Each now uses an assertion that matches what is being expected: a full
message (EqualError), a substring (ErrorContains) or an error value
(ErrorIs).

mx_test.go needed more than a mechanical swap. CheckMX returns the
resolver's *net.DNSError unchanged -- "lookup githubexists.com on
...:53: no such host" -- so it never contained ErrNoSuchHost ("Mail
server does not exist"); it is Verify that maps it. Asserting the
message text would also bake in the resolver address, which varies by
environment, so the test now asserts the DNS semantics via ErrorAs and
IsNotFound.

Verified: each of the six fails when its expected value is mutated
(IsNotFound -> False, ErrNoSuchHost -> ErrTimeout, ECONNREFUSED ->
ETIMEDOUT, 404 -> 403, 500 -> 503, and one character of the JSON error).
70 offline tests pass under -race. The six touched tests reach the
network only via DNS, or via a SOCKS dial to 127.0.0.1; none opens an
SMTP connection.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* chore(lint): enable usestdlibvars and intrange

Both catch a legacy literal where the standard library offers a named
form, and both fix mechanically with no behaviour change. Seven findings:

  gravatar.go, handler.go        "GET" -> http.MethodGet
  gravatar.go                    200 -> http.StatusOK
  cmd/apiserver/main.go (x2)     500 -> http.StatusInternalServerError
  smtp.go, schedule_test.go      for i := 0; i < n; i++ -> for range n

Enabling the linters rather than only editing the call sites is the
point: with the baseline at zero there is nothing to migrate around, so
the rule is enforced for new code instead of being reapplied by hand.

Turning on every usestdlibvars opt-in as well (time-weekday, time-month,
time-layout, crypto-hash, default-rpc-path, sql-isolation-level,
tls-signature-scheme, constant-kind) surfaces nothing further, so the
defaults are left in place.

Two adjacent linters were surveyed and deliberately not enabled:

  - nilnil (1): smtp.go's `return nil, nil` is the contract CheckSMTP
    documents for a disabled SMTP check. Changing it is a public API
    decision, not a cleanup.
  - godot (3): requiring declaration comments to end in a period is a
    style preference that would bind every future contribution.

Verified: golangci-lint run reports 0, go build and go vet are clean,
70 offline tests pass under -race, and GenerateRandomEmail still yields
32 alphanumeric bytes after the loop rewrite.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-09-11 10:09:20 +08:00

51 lines
1.2 KiB
Go

package emailverifier
import (
"context"
"errors"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCheckMxOK(t *testing.T) {
domain := "github.com"
mx, err := verifier.CheckMX(domain)
require.NoError(t, err)
assert.True(t, mx.HasMXRecord)
}
func TestCheckNoMxOK(t *testing.T) {
domain := "githubexists.com"
mx, err := verifier.CheckMX(domain)
assert.Nil(t, mx)
// CheckMX surfaces the resolver error as-is; it is Verify that maps it to
// ErrNoSuchHost. Assert the DNS semantics rather than the message text,
// which carries the resolver address and so varies by environment.
var dnsErr *net.DNSError
require.ErrorAs(t, err, &dnsErr)
assert.True(t, dnsErr.IsNotFound, "expected NXDOMAIN, got %v", err)
}
func TestCheckMx_WithCustomResolver(t *testing.T) {
domain := "github.com"
wantErr := errors.New("custom resolver dial invoked")
customResolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, wantErr
},
}
v := NewVerifier().Resolver(customResolver)
mx, err := v.CheckMX(domain)
assert.Nil(t, mx)
assert.ErrorContains(t, err, wantErr.Error())
}