package offsite import ( "io" "log" "path/filepath" "testing" "time" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // R-70 detector tests — four states + the applied-wins precedence + the demo-felhom // applied+stale-staged edge. Fixtures are the real store (t.TempDir SQLite), timestamps // controlled via the test-only back-dater (no sleeps). func newDeliveryStore(t *testing.T) *store.Store { t.Helper() st, err := store.New(filepath.Join(t.TempDir(), "d.db"), log.New(io.Discard, "", 0)) if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) return st } const ( reportWithOffsite = `{"health":{"status":"ok"},"offsite":{"enabled":true,"escrow_state":"escrowed"}}` reportNoOffsite = `{"health":{"status":"ok"}}` ) func TestDeliveryState_Applied(t *testing.T) { st := newDeliveryStore(t) if err := st.SaveReport("c1", []byte(reportWithOffsite)); err != nil { t.Fatal(err) } if err := st.SaveOneTimeSecret("c1", "x"); err != nil { t.Fatal(err) } if _, err := st.ConsumeOneTimeSecret("c1"); err != nil { t.Fatal(err) } got, err := DeliveryStateFor(st, "c1") if err != nil { t.Fatal(err) } if got.State != DeliveryApplied { t.Fatalf("state = %s, want applied", got.State) } if !got.StaleStagedSince.IsZero() { t.Fatalf("consumed secret must not flag stale-staged, got %v", got.StaleStagedSince) } if got.Since.IsZero() { t.Fatal("applied state must carry the report timestamp (mióta)") } } // The demo-felhom shape: applied via an earlier generation + an unconsumed secret staged since. // Precedence: applied WINS, and the stale staged secret becomes a VISIBLE info flag — never a // state downgrade, never a heal trigger. func TestDeliveryState_AppliedWithStaleStaged(t *testing.T) { st := newDeliveryStore(t) if err := st.SaveReport("c1", []byte(reportWithOffsite)); err != nil { t.Fatal(err) } if err := st.SaveOneTimeSecret("c1", "stale"); err != nil { t.Fatal(err) } if err := st.SetOneTimeSecretTimesForTest("c1", "2026-07-21 08:29:29", ""); err != nil { t.Fatal(err) } got, err := DeliveryStateFor(st, "c1") if err != nil { t.Fatal(err) } if got.State != DeliveryApplied { t.Fatalf("precedence broken: state = %s, want applied (applied wins over staged)", got.State) } want := time.Date(2026, 7, 21, 8, 29, 29, 0, time.UTC) if !got.StaleStagedSince.Equal(want) { t.Fatalf("StaleStagedSince = %v, want %v (the invisible plumbing must be visible)", got.StaleStagedSince, want) } } // The burned-credential shape (F10): consumed, and every report since carries no offbox. func TestDeliveryState_ConsumedAwaitingApply(t *testing.T) { st := newDeliveryStore(t) if err := st.SaveOneTimeSecret("c1", "x"); err != nil { t.Fatal(err) } // consumed 2h ago; the reports below (received "now") all come AFTER it if err := st.SetOneTimeSecretTimesForTest("c1", "2026-01-01 00:00:00", "2026-01-01 00:03:00"); err != nil { t.Fatal(err) } for i := 0; i < 5; i++ { if err := st.SaveReport("c1", []byte(reportNoOffsite)); err != nil { t.Fatal(err) } } got, err := DeliveryStateFor(st, "c1") if err != nil { t.Fatal(err) } if got.State != DeliveryConsumedAwaitingApply { t.Fatalf("state = %s, want consumed_awaiting_apply", got.State) } if got.ReportsSinceConsume != 5 || got.OffsiteReportsSinceConsume != 0 { t.Fatalf("counts = %d/%d, want 5/0", got.ReportsSinceConsume, got.OffsiteReportsSinceConsume) } if want := time.Date(2026, 1, 1, 0, 3, 0, 0, time.UTC); !got.Since.Equal(want) { t.Fatalf("Since = %v, want consumed_at %v", got.Since, want) } } // The since-filter: reports received BEFORE consumed_at must not count toward the trigger. func TestDeliveryState_ReportsBeforeConsumeNotCounted(t *testing.T) { st := newDeliveryStore(t) for i := 0; i < 3; i++ { if err := st.SaveReport("c1", []byte(reportNoOffsite)); err != nil { t.Fatal(err) } } if err := st.SaveOneTimeSecret("c1", "x"); err != nil { t.Fatal(err) } // consumed FAR in the future relative to the rows above → zero reports since if err := st.SetOneTimeSecretTimesForTest("c1", "2099-01-01 00:00:00", "2099-01-01 00:00:01"); err != nil { t.Fatal(err) } got, err := DeliveryStateFor(st, "c1") if err != nil { t.Fatal(err) } if got.State != DeliveryConsumedAwaitingApply || got.ReportsSinceConsume != 0 { t.Fatalf("state/count = %s/%d, want consumed_awaiting_apply/0 (pre-consume reports must not count)", got.State, got.ReportsSinceConsume) } } func TestDeliveryState_StagedAwaitingConsume(t *testing.T) { st := newDeliveryStore(t) if err := st.SaveReport("c1", []byte(reportNoOffsite)); err != nil { t.Fatal(err) } if err := st.SaveOneTimeSecret("c1", "x"); err != nil { t.Fatal(err) } got, err := DeliveryStateFor(st, "c1") if err != nil { t.Fatal(err) } if got.State != DeliveryStagedAwaitingConsume { t.Fatalf("state = %s, want staged_awaiting_consume", got.State) } if got.Since.IsZero() { t.Fatal("staged state must carry created_at (mióta)") } } func TestDeliveryState_NoSecret(t *testing.T) { st := newDeliveryStore(t) if err := st.SaveReport("c1", []byte(reportNoOffsite)); err != nil { t.Fatal(err) } got, err := DeliveryStateFor(st, "c1") if err != nil { t.Fatal(err) } if got.State != DeliveryNoSecret { t.Fatalf("state = %s, want no_secret (mis-state must render needs-attention, never applied)", got.State) } }