test / test (push) Waiting to run
The panel's signer is the only one. Gone: the opendkim package and its
configuration, the syslog-relay and opendkim programs, /dev/log and
/run/opendkim, the reload by USR1 through supervisord
(internal/supervisor/supervisor.go), internal/syslogrelay, the port of
OpenDKIM's From: parser (internal/milter/mailparse.go and its probe,
harness and test/upstream scripts), OPENDKIM_SOCKET and OPENDKIM_DIR.
Keys: internal/domain/keys.go (Keys, NewKeys, DKIM_DIR=/data/dkim) keeps
the keys and writes a selector table, /data/dkim/selectors ("<name>
<selector>"), which the signer reads in place of the KeyTable - it reads
no database, as the sender check reads none. The table is written before
the signer serves (EnsureServerSigned moved there from the notifier).
Keys and table 0600, directories 0700, the panel's. The entrypoint copies
/data/opendkim/keys once, as the panel user, into /data/dkim.new renamed
when whole, and stops the start if the copy fails; /data/opendkim is left
for a rollback to 2.7.x.
Sender check: one RFC 5322 address as a mail program reads it, held
against the bindings; the 256-byte refusal and the texts naming OpenDKIM
go. Allowed() folded the domain through strings.ToLower, which makes the
Kelvin sign a "k" - the comparison with OpenDKIM's reading had hidden it;
it folds ASCII only now.
Health: two dkim-signer rows (applications, notifications) in Milter
sockets, opendkim out of the liveness set; the card no longer takes any
row with a note for the spam filter. Texts as in the design commit.
KILL stays in deploy/docker-compose.yml: measured on the test server,
root without it cannot signal a process of another uid - supervisord
stopping the panel, Postfix's master its children. The plan's "only use
was the reload" was wrong; the comment now gives the real uses.
e2e: one signature, the panel's, verified by dkimpy; the /data/dkim modes;
the OpenDKIM log and process checks, testNeverSignedForAnother and the
cited opendkim, libopendkim11 and libmilter1.0.1 go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
5.4 KiB
Go
130 lines
5.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mixeme/selfpost/internal/dnscheck"
|
|
"github.com/mixeme/selfpost/internal/health"
|
|
)
|
|
|
|
// checksFixture is one reading of every server check, shaped as healthChecks
|
|
// returns it.
|
|
func checksFixture() healthReadings {
|
|
return healthReadings{
|
|
Processes: []health.Process{
|
|
{Name: "panel", State: "RUNNING", Status: health.StatusOK},
|
|
{Name: "postfix", State: "FATAL", Status: health.StatusError},
|
|
},
|
|
ProcessError: false,
|
|
ProcessStatus: health.StatusError,
|
|
QueueSummary: "Mail queue is empty",
|
|
QueueError: "",
|
|
QueueStatus: health.StatusOK,
|
|
Machine: health.Machine{Status: health.StatusUnknown},
|
|
Cert: health.Certificate{Subject: "mail.example.org", DaysLeft: 12, Status: health.StatusWarn, Detail: "Expires soon."},
|
|
Sockets: []health.Socket{
|
|
{Name: "dkim-signer", Present: true, Status: health.StatusOK},
|
|
{Name: "send-log", Present: false, Status: health.StatusWarn},
|
|
},
|
|
SocketStatus: health.StatusWarn,
|
|
Hostname: "mail.example.org",
|
|
PTR: dnscheck.Result{Status: health.StatusOK},
|
|
}
|
|
}
|
|
|
|
// Overview is one verdict per check: six cards in the order of the tables on
|
|
// Health, each saying in words what its table would, and each leading there.
|
|
func TestHealthCards(t *testing.T) {
|
|
cards := healthCards(checksFixture())
|
|
want := []struct{ name, level, value, sub, href string }{
|
|
{"Machine", "warn", "No reading", "", "/server/health"},
|
|
{"Processes", "fail", "1 of 2 running", "postfix", "/server/health"},
|
|
{"TLS certificate", "warn", "Expires in 12 days", "mail.example.org", "/server/health"},
|
|
{"Queue", "", "Empty", "outbound", "/outbound/queue"},
|
|
{"Milter sockets", "warn", "1 of 2 answering", "send-log", "/server/health"},
|
|
{"Reverse DNS", "", "Forward = reverse", "mail.example.org", "/server/health"},
|
|
}
|
|
if len(cards) != len(want) {
|
|
t.Fatalf("%d cards, want %d", len(cards), len(want))
|
|
}
|
|
for i, w := range want {
|
|
c := cards[i]
|
|
if c.Name != w.name || c.Level != w.level || c.Value != w.value || c.Sub != w.sub || c.Href != w.href || c.Icon == "" {
|
|
t.Errorf("card %d = %+v, want %+v", i, c, w)
|
|
}
|
|
}
|
|
|
|
// A check that could not run must not look fine.
|
|
c := checksFixture()
|
|
c.ProcessError = true
|
|
c.ProcessStatus = health.StatusUnknown
|
|
c.PTR = dnscheck.Result{Status: health.StatusUnknown}
|
|
cards = healthCards(c)
|
|
if cards[1].Level != "warn" || cards[1].Value != "Could not be read" || cards[5].Level != "warn" || cards[5].Value != "Not checked" {
|
|
t.Errorf("unverified checks render as %+v and %+v", cards[1], cards[5])
|
|
}
|
|
}
|
|
|
|
// The Queue card has room for a few words: postqueue's own last line is cut
|
|
// down to what it says.
|
|
func TestQueueCardValue(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"": "Empty",
|
|
"Mail queue is empty": "Empty",
|
|
"3 Kbytes in 2 Requests.": "2 queued",
|
|
"1 Kbytes in 1 Request.": "1 queued",
|
|
"something postfix invented": "something postfix invented",
|
|
} {
|
|
if got := queueCardValue(in); got != want {
|
|
t.Errorf("queueCardValue(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The Processes card counts the programs that are meant to stay up. The one-shot
|
|
// reload helper idles between runs: it is neither running nor missing.
|
|
func TestProcessesCardLeavesOutTheIdleOneShot(t *testing.T) {
|
|
c := checksFixture()
|
|
c.Processes = []health.Process{
|
|
{Name: "panel", State: "RUNNING", Status: health.StatusOK},
|
|
{Name: "postfix", State: "RUNNING", Status: health.StatusOK},
|
|
{Name: "postfix-reload", State: "STOPPED", Status: health.StatusOK, OneShot: true},
|
|
}
|
|
c.ProcessStatus = health.StatusOK
|
|
card := healthCards(c)[1]
|
|
if card.Value != "2 of 2 running" || card.Sub != "all programs" {
|
|
t.Errorf("card = %q / %q, want 2 of 2 running / all programs", card.Value, card.Sub)
|
|
}
|
|
|
|
c.Processes = []health.Process{
|
|
{Name: "panel", State: "RUNNING", Status: health.StatusOK},
|
|
{Name: "postfix-reload", State: "FATAL", Status: health.StatusError, OneShot: true},
|
|
}
|
|
card = healthCards(c)[1]
|
|
if card.Value != "1 of 1 running" || card.Sub != "postfix-reload" {
|
|
t.Errorf("card = %q / %q, want 1 of 1 running / postfix-reload", card.Value, card.Sub)
|
|
}
|
|
}
|
|
|
|
// An idle one-shot helper is "on demand", not "stopped"; a daemon keeps its
|
|
// state word, and a one-shot in trouble keeps its own.
|
|
func TestProcessRowOfAnIdleOneShotSaysOnDemand(t *testing.T) {
|
|
idle := processRow(health.Process{Name: "postfix-reload", State: "STOPPED", Detail: "Not started", Status: health.StatusOK, OneShot: true})
|
|
if idle.State.Label != "on demand" || idle.State.Status != "off" || !strings.Contains(idle.Detail, "not run since the container started") {
|
|
t.Errorf("idle one-shot = %+v", idle)
|
|
}
|
|
ran := processRow(health.Process{Name: "postfix-reload", State: "EXITED", Detail: "Oct 10 08:42 PM", Status: health.StatusOK, OneShot: true})
|
|
if ran.State.Label != "on demand" || !strings.HasSuffix(ran.Detail, "last run: Oct 10 08:42 PM") {
|
|
t.Errorf("one-shot that ran = %+v", ran)
|
|
}
|
|
bad := processRow(health.Process{Name: "postfix-reload", State: "FATAL", Detail: "Exited too quickly", Status: health.StatusError, OneShot: true})
|
|
if bad.State.Label != "fatal" || bad.State.Status != "error" || bad.Detail != "Exited too quickly" {
|
|
t.Errorf("one-shot in trouble = %+v", bad)
|
|
}
|
|
daemon := processRow(health.Process{Name: "postfix", State: "STOPPED", Detail: "Oct 10 08:42 PM", Status: health.StatusError})
|
|
if daemon.State.Label != "stopped" || daemon.State.Status != "error" {
|
|
t.Errorf("stopped daemon = %+v", daemon)
|
|
}
|
|
}
|