hub v0.71.0: paired recovery mails (F11), prefs seeding at claim + empty-email no-clobber (F12), priority headers + operator test leg (F14-light)
This commit is contained in:
@@ -199,8 +199,23 @@ func (e *Engine) ResetToUnclaimed(cc *store.CustomerConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// defaultSeedEvents is the enabled_events set seeded at claim (v0.71.0, audit F12) — critical-only:
|
||||
// no node_stale (too chatty), no *_recovered (the recovery pairing gate handles those and never
|
||||
// consults enabled_events). Viktor may adjust the list at review.
|
||||
var defaultSeedEvents = []string{
|
||||
"node_down",
|
||||
"backup_failed",
|
||||
"disk_critical",
|
||||
"host_disk_critical",
|
||||
"storage_fill_critical",
|
||||
"offbox_repo_orphaned",
|
||||
}
|
||||
|
||||
// MarkClaimed records a controller-reported successful claim and sends the one-time confirmation
|
||||
// email on the unclaimed→claimed transition (idempotent — repeated reports are no-ops).
|
||||
// email on the unclaimed→claimed transition (idempotent — repeated reports are no-ops). On the
|
||||
// transition it also seeds notification prefs from the registered email (v0.71.0, audit F12) so a
|
||||
// claimed customer can no longer be silently unnotifiable — insert-if-absent, never overwriting a
|
||||
// customer-edited row, and never failing the claim (notification plumbing must not gate claiming).
|
||||
func (e *Engine) MarkClaimed(cc *store.CustomerConfig) error {
|
||||
transitioned, err := e.Store.MarkClaimed(cc.CustomerID)
|
||||
if err != nil {
|
||||
@@ -210,6 +225,13 @@ func (e *Engine) MarkClaimed(cc *store.CustomerConfig) error {
|
||||
return nil
|
||||
}
|
||||
e.logf("[INFO] [claim] customer %s CLAIMED its dashboard (password set by the customer)", cc.CustomerID)
|
||||
if seeded, err := e.Store.SeedNotificationPrefs(cc.CustomerID, cc.Email, defaultSeedEvents); err != nil {
|
||||
e.logf("[WARN] [claim] notification-prefs seed for %s failed (claim unaffected): %v", cc.CustomerID, err)
|
||||
} else if seeded {
|
||||
e.logf("[INFO] [claim] notification prefs seeded for %s from the registered email (default critical set)", cc.CustomerID)
|
||||
} else {
|
||||
e.logf("[INFO] [claim] notification prefs for %s left untouched (row exists or no registered email)", cc.CustomerID)
|
||||
}
|
||||
if cc.Email != "" {
|
||||
if err := e.Mailer.SendClaimEmail(string(EmailClaimed), cc.CustomerID, cc.Email, cc.Domain, ""); err != nil {
|
||||
e.logf("[WARN] [claim] claimed-confirmation email to %s failed: %v", cc.CustomerID, err)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package claim
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
)
|
||||
|
||||
// TestMarkClaimed_SeedsNotificationPrefs (v0.71.0 F12, Scenario D): the unclaimed→claimed
|
||||
// transition seeds a customer_notifications row from the registered email with the default
|
||||
// critical-only event set; a pre-existing (customer-edited) row is NEVER modified.
|
||||
// Companion red-proof: seeding via SaveNotificationPrefs (upsert) makes the second half fail.
|
||||
func TestMarkClaimed_SeedsNotificationPrefs(t *testing.T) {
|
||||
e, st, _ := newTestEngine(t)
|
||||
if _, err := e.EnsureIssued(cust()); err != nil {
|
||||
t.Fatalf("EnsureIssued: %v", err)
|
||||
}
|
||||
if err := e.MarkClaimed(cust()); err != nil {
|
||||
t.Fatalf("MarkClaimed: %v", err)
|
||||
}
|
||||
prefs, err := st.GetNotificationPrefs("c1")
|
||||
if err != nil || prefs == nil {
|
||||
t.Fatalf("prefs must be seeded at claim, got %+v err=%v", prefs, err)
|
||||
}
|
||||
if prefs.Email != "owner@example.hu" {
|
||||
t.Fatalf("seeded email = %q, want the registered address", prefs.Email)
|
||||
}
|
||||
if len(prefs.EnabledEvents) != len(defaultSeedEvents) {
|
||||
t.Fatalf("seeded events = %v, want the default set %v", prefs.EnabledEvents, defaultSeedEvents)
|
||||
}
|
||||
for i, ev := range defaultSeedEvents {
|
||||
if prefs.EnabledEvents[i] != ev {
|
||||
t.Fatalf("seeded events = %v, want %v", prefs.EnabledEvents, defaultSeedEvents)
|
||||
}
|
||||
}
|
||||
if prefs.CooldownHours != 6 {
|
||||
t.Fatalf("seeded cooldown = %d, want 6", prefs.CooldownHours)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarkClaimed_NeverOverwritesExistingPrefs: a customer-edited row survives a claim
|
||||
// byte-identical (re-claim after RESET is the natural trigger).
|
||||
func TestMarkClaimed_NeverOverwritesExistingPrefs(t *testing.T) {
|
||||
e, st, _ := newTestEngine(t)
|
||||
if err := st.SaveNotificationPrefs("c1", "edited@example.hu", []string{"node_down"}, 12); err != nil {
|
||||
t.Fatalf("pre-existing prefs: %v", err)
|
||||
}
|
||||
if _, err := e.EnsureIssued(cust()); err != nil {
|
||||
t.Fatalf("EnsureIssued: %v", err)
|
||||
}
|
||||
if err := e.MarkClaimed(cust()); err != nil {
|
||||
t.Fatalf("MarkClaimed: %v", err)
|
||||
}
|
||||
prefs, _ := st.GetNotificationPrefs("c1")
|
||||
if prefs == nil || prefs.Email != "edited@example.hu" || len(prefs.EnabledEvents) != 1 || prefs.CooldownHours != 12 {
|
||||
t.Fatalf("claim seed MODIFIED an existing row (upsert bug): %+v", prefs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarkClaimed_EmptyEmail_NoRow_ClaimSucceeds: no registered email → no seed, and the claim
|
||||
// itself still succeeds (notification plumbing must never gate claiming).
|
||||
func TestMarkClaimed_EmptyEmail_NoRow_ClaimSucceeds(t *testing.T) {
|
||||
e, st, _ := newTestEngine(t)
|
||||
noMail := &store.CustomerConfig{CustomerID: "c1", Email: "", Domain: "example.hu"}
|
||||
if _, err := e.EnsureIssued(cust()); err != nil { // issue needs an email to deliver the code
|
||||
t.Fatalf("EnsureIssued: %v", err)
|
||||
}
|
||||
if err := e.MarkClaimed(noMail); err != nil {
|
||||
t.Fatalf("MarkClaimed with empty email must succeed: %v", err)
|
||||
}
|
||||
if prefs, _ := st.GetNotificationPrefs("c1"); prefs != nil {
|
||||
t.Fatalf("empty registered email must seed nothing, got %+v", prefs)
|
||||
}
|
||||
// The transition happened — a second call is a no-op (unchanged idempotency).
|
||||
if err := e.MarkClaimed(noMail); err != nil {
|
||||
t.Fatalf("idempotent re-claim: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user