Every Web Push to an iPhone or a Mac was refused, silently as far as the person waiting for the reminder could tell. The cause was the VAPID "sub" claim: on an instance with no mail configured it fell back to mailto:verdande@example.invalid, and Apple answers 403 Forbidden to a mailto whose domain is made up. Verified against web.push.apple.com on the live subscriptions — that mailto gets a 403, the instance's https URL a 201. So the subject is now a real mailto when SMTP.From is a real address, and otherwise the instance's own https base URL — which is a valid VAPID subject, points at something that exists, and every push service including Apple accepts. Two things found alongside it, both fixed: - A successful push did not clear the failure count. A subscription drops at ten failures, and all four live ones sat at nine from this bug — one flaky push from being deleted despite working. MarkPushDelivered zeroes the count and moves last_used_at on every delivery, so a subscription that recovers is not still carrying its old failures toward the cliff. - A reminder with no project made a notification whose URL was "/projekt/" — a dead link. Empty project now points at the app root. Test: the subject is a real mailto when there is one, and the https URL when there is not — including for the localhost default, which is not a real address. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
34 lines
1.3 KiB
Go
34 lines
1.3 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/kristianwind/verdande/internal/config"
|
|
)
|
|
|
|
// The VAPID subject has to be one a push service will accept. Apple answers 403 to
|
|
// a mailto with a made-up domain, which is what the no-mail fallback used to be, so
|
|
// every push to an Apple device was refused. With no real address, the instance's
|
|
// own https URL is used instead — valid, real, and accepted.
|
|
func TestVAPIDSubject(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
from string
|
|
baseURL string
|
|
want string
|
|
}{
|
|
{"a real address is used as-is", "todo@example.dk", "https://verdande.example.dk", "mailto:todo@example.dk"},
|
|
{"no mail falls back to the https url", "", "https://verdande.example.dk", "https://verdande.example.dk"},
|
|
{"the localhost default is not a real address", "verdande@localhost", "https://verdande.example.dk", "https://verdande.example.dk"},
|
|
{"a plain http dev url still serves as a subject", "", "http://localhost:8080", "http://localhost:8080"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s := &Server{cfg: &config.Config{BaseURL: tc.baseURL, SMTP: config.SMTP{From: tc.from}}}
|
|
if got := s.vapidSubject(); got != tc.want {
|
|
t.Errorf("vapidSubject() = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|