The repositories moved off LINUXexpert-org. Here that is not a documentation change: the old organisation was the module path, so it is declared in go.mod and repeated in every internal import. Leaving it would have been worse than a stale link. GitHub redirects the repository, but a go.mod whose module line disagrees with the path it was fetched from is an error rather than a redirect, so `go get` on the new address would have failed against the old declaration. go.mod, 34 files of imports, and the repository links in README and ARCHITECTURE. go mod tidy leaves go.sum untouched -- no dependency moved, only our own path.
103 lines
4.1 KiB
Go
103 lines
4.1 KiB
Go
// SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package main
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/Coffey-Labs/stalwart-migrator/internal/checkpoint"
|
|
"github.com/Coffey-Labs/stalwart-migrator/internal/validate"
|
|
)
|
|
|
|
// `report` re-reads what the run recorded rather than re-checking, so the
|
|
// mapping from checkpointed step back to verdict is the whole command. The
|
|
// case that matters is a failure staying a failure: a migration that lost an
|
|
// account must not read as clean the next morning.
|
|
|
|
func TestReportFromSteps(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
step checkpoint.StepRecord
|
|
want validate.Status
|
|
block bool
|
|
}{
|
|
{
|
|
name: "a completed comparison is a pass",
|
|
step: checkpoint.StepRecord{Phase: checkpoint.PhaseValidate, Name: "content-integrity", Status: checkpoint.StepDone, StepOutcome: checkpoint.StepOutcome{Detail: "12 accounts checked"}},
|
|
want: validate.StatusOK,
|
|
},
|
|
{
|
|
name: "a failing verdict survives the round trip",
|
|
step: checkpoint.StepRecord{Phase: checkpoint.PhaseValidate, Name: "content-integrity", Status: checkpoint.StepDone, StepOutcome: checkpoint.StepOutcome{Verdict: string(validate.StatusFail), Detail: "missing accounts: [email protected]"}},
|
|
want: validate.StatusFail,
|
|
block: true,
|
|
},
|
|
{
|
|
name: "a skipped check stays skipped, not a pass",
|
|
step: checkpoint.StepRecord{Phase: checkpoint.PhaseValidate, Name: "content-integrity", Status: checkpoint.StepDone, StepOutcome: checkpoint.StepOutcome{Verdict: string(validate.StatusSkip), Detail: "no snapshot"}},
|
|
want: validate.StatusSkip,
|
|
},
|
|
{
|
|
name: "a step that errored is a failure",
|
|
step: checkpoint.StepRecord{Phase: checkpoint.PhaseValidate, Name: "content-integrity", Status: checkpoint.StepFailed, Error: "connection refused"},
|
|
want: validate.StatusFail,
|
|
block: true,
|
|
},
|
|
{
|
|
name: "a step that never finished is a failure, not a pass",
|
|
step: checkpoint.StepRecord{Phase: checkpoint.PhaseValidate, Name: "content-integrity", Status: checkpoint.StepRunning},
|
|
want: validate.StatusFail,
|
|
block: true,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := reportFromSteps(&checkpoint.RunState{Steps: []checkpoint.StepRecord{tc.step}})
|
|
if len(got.Results) != 1 {
|
|
t.Fatalf("got %d results, want 1", len(got.Results))
|
|
}
|
|
if got.Results[0].Status != tc.want {
|
|
t.Fatalf("status = %q, want %q", got.Results[0].Status, tc.want)
|
|
}
|
|
if got.Blocking() != tc.block {
|
|
t.Fatalf("Blocking() = %v, want %v", got.Blocking(), tc.block)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReportFromStepsIgnoresOtherPhases(t *testing.T) {
|
|
rs := &checkpoint.RunState{Steps: []checkpoint.StepRecord{
|
|
{Phase: checkpoint.PhaseCutover, Name: "start-service", Status: checkpoint.StepDone},
|
|
{Phase: checkpoint.PhasePreflight, Name: "disk-space", Status: checkpoint.StepFailed, Error: "nope"},
|
|
{Phase: checkpoint.PhaseValidate, Name: "content-integrity", Status: checkpoint.StepDone, StepOutcome: checkpoint.StepOutcome{Detail: "ok"}},
|
|
}}
|
|
got := reportFromSteps(rs)
|
|
if len(got.Results) != 1 || got.Results[0].Name != "content-integrity" {
|
|
t.Fatalf("expected only the validate phase, got %+v", got.Results)
|
|
}
|
|
// A failed preflight step must not leak into the validation verdict.
|
|
if got.Blocking() {
|
|
t.Fatal("a failure in another phase must not make validation look failed")
|
|
}
|
|
}
|
|
|
|
func TestReportFromStepsOnARunThatNeverValidated(t *testing.T) {
|
|
rs := &checkpoint.RunState{Steps: []checkpoint.StepRecord{
|
|
{Phase: checkpoint.PhaseCutover, Name: "start-service", Status: checkpoint.StepDone},
|
|
}}
|
|
if got := reportFromSteps(rs); len(got.Results) != 0 {
|
|
t.Fatalf("expected no results, got %+v", got.Results)
|
|
}
|
|
}
|
|
|
|
func TestReportFromStepsKeepsTheError(t *testing.T) {
|
|
rs := &checkpoint.RunState{Steps: []checkpoint.StepRecord{
|
|
{Phase: checkpoint.PhaseValidate, Name: "content-integrity", Status: checkpoint.StepFailed, StepOutcome: checkpoint.StepOutcome{Detail: "reached the instance"}, Error: "401 unauthorized"},
|
|
}}
|
|
got := reportFromSteps(rs)
|
|
if d := got.Results[0].Detail; d != "reached the instance (error: 401 unauthorized)" {
|
|
t.Fatalf("detail = %q, want it to carry both the detail and the error", d)
|
|
}
|
|
}
|