Initial feature-complete implementation of the v1.0 specification (phases 0-11 of `docs/implementation-plan.md`). Added: - Panel (Go, single static binary) with SQLite persistence, one-time - Domain management with per-domain DKIM (RSA-2048, generated in pure Go) and - Application (sender identity) management: SASL credentials via `sasldb2`, - Full Postfix relay config generated from env at container start: SMTPS 465, - Journal milter (pure Go, `go-milter`) recording every send to `send_log`; … (6 more in CHANGELOG) Full detail: CHANGELOG.md (0.1.0).
87 lines
2.1 KiB
Go
87 lines
2.1 KiB
Go
package domain
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestWriteLoadPrivateKeyRoundtrip(t *testing.T) {
|
|
key, err := generateDKIMKey()
|
|
if err != nil {
|
|
t.Fatalf("generateDKIMKey: %v", err)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "selfpost.private")
|
|
if err := writePrivateKeyPEM(path, key); err != nil {
|
|
t.Fatalf("writePrivateKeyPEM: %v", err)
|
|
}
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("stat: %v", err)
|
|
}
|
|
if perm := info.Mode().Perm(); perm != 0o640 {
|
|
t.Errorf("key perm = %o, want 0640", perm)
|
|
}
|
|
|
|
loaded, err := loadPrivateKeyPEM(path)
|
|
if err != nil {
|
|
t.Fatalf("loadPrivateKeyPEM: %v", err)
|
|
}
|
|
if loaded.N.Cmp(key.N) != 0 || loaded.E != key.E {
|
|
t.Error("loaded key does not match generated key")
|
|
}
|
|
}
|
|
|
|
func TestLoadPrivateKeyRejectsGarbage(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "bad.private")
|
|
if err := os.WriteFile(path, []byte("not a pem"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := loadPrivateKeyPEM(path); err == nil {
|
|
t.Error("expected error for non-PEM key file")
|
|
}
|
|
}
|
|
|
|
func TestDKIMRecord(t *testing.T) {
|
|
key, err := generateDKIMKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec, err := dkimRecord("selfpost", "example.com", &key.PublicKey)
|
|
if err != nil {
|
|
t.Fatalf("dkimRecord: %v", err)
|
|
}
|
|
if rec.Name != "selfpost._domainkey.example.com" {
|
|
t.Errorf("record name = %q", rec.Name)
|
|
}
|
|
for _, want := range []string{"v=DKIM1", "h=sha256", "k=rsa", "p="} {
|
|
if !strings.Contains(rec.Value, want) {
|
|
t.Errorf("record value %q missing %q", rec.Value, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteFileAtomicOverwrite(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "f")
|
|
if err := writeFileAtomic(path, []byte("one"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := writeFileAtomic(path, []byte("two"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != "two" {
|
|
t.Errorf("content = %q, want %q", got, "two")
|
|
}
|
|
// No stray temp files left behind in the directory.
|
|
entries, _ := os.ReadDir(filepath.Dir(path))
|
|
if len(entries) != 1 {
|
|
t.Errorf("expected 1 file after atomic writes, found %d", len(entries))
|
|
}
|
|
}
|