-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmain_encryption_startup_guard_test.go
More file actions
239 lines (219 loc) · 8.94 KB
/
Copy pathmain_encryption_startup_guard_test.go
File metadata and controls
239 lines (219 loc) · 8.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package main
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/bootjp/elastickv/internal/encryption"
)
// stubGapEngine satisfies encryptionGapEngine for the
// runSidecarBehindRaftLogGuard tests.
type stubGapEngine struct {
appliedIndex uint64
scanner encryption.EncryptionRelevantScanner
}
func (s *stubGapEngine) AppliedIndex() uint64 { return s.appliedIndex }
func (s *stubGapEngine) EncryptionScanner() encryption.EncryptionRelevantScanner {
return s.scanner
}
// stubScanner is a fake encryption.EncryptionRelevantScanner that
// returns a fixed verdict. Lets the guard tests exercise the
// hit / no-hit / error branches without a real raftengine.
type stubScanner struct {
hit bool
err error
}
func (s *stubScanner) HasEncryptionRelevantEntryInRange(_, _ uint64) (bool, error) {
return s.hit, s.err
}
// writeMinimalSidecar writes a valid §5.1 sidecar with the
// supplied RaftAppliedIndex into a freshly created temp dir, and
// returns the sidecar path.
func writeMinimalSidecar(t *testing.T, raftAppliedIdx uint64) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, encryption.SidecarFilename)
sc := &encryption.Sidecar{
Version: encryption.SidecarVersion,
RaftAppliedIndex: raftAppliedIdx,
Keys: map[string]encryption.SidecarKey{},
}
if err := encryption.WriteSidecar(path, sc); err != nil {
t.Fatalf("WriteSidecar: %v", err)
}
return path
}
// TestCheckSidecarBehindRaftLog_DisabledNoop pins the
// fast-skip when --encryption-enabled is off. Even with a stale
// sidecar on disk, the guard MUST return nil and never read it.
func TestCheckSidecarBehindRaftLog_DisabledNoop(t *testing.T) {
sidecarPath := writeMinimalSidecar(t, 0) // way behind any engine
err := checkSidecarBehindRaftLog(nil, 1, sidecarPath, false)
if err != nil {
t.Fatalf("guard must skip when encryptionEnabled=false; got %v", err)
}
}
// TestCheckSidecarBehindRaftLog_NoSidecarPathNoop pins the
// empty-path fast-skip.
func TestCheckSidecarBehindRaftLog_NoSidecarPathNoop(t *testing.T) {
err := checkSidecarBehindRaftLog(nil, 1, "", true)
if err != nil {
t.Fatalf("guard must skip on empty sidecar path; got %v", err)
}
}
// TestCheckSidecarBehindRaftLog_SidecarAbsentNoop pins the
// "no on-disk sidecar" fast-skip. A configured sidecar path with
// no file means bootstrap hasn't committed — no gap to refuse on.
func TestCheckSidecarBehindRaftLog_SidecarAbsentNoop(t *testing.T) {
dir := t.TempDir()
err := checkSidecarBehindRaftLog(nil, 1, filepath.Join(dir, "nonexistent.json"), true)
if err != nil {
t.Fatalf("guard must skip when sidecar file is absent; got %v", err)
}
}
// TestCheckSidecarBehindRaftLog_SidecarStatError surfaces a real
// I/O error (path with NUL byte) as a wrapped error rather than
// silently classifying it as "sidecar absent".
func TestCheckSidecarBehindRaftLog_SidecarStatError(t *testing.T) {
err := checkSidecarBehindRaftLog(nil, 1, "/tmp/elastickv-test/\x00invalid", true)
if err == nil {
t.Fatal("guard must surface I/O error from sidecar stat")
}
if errors.Is(err, os.ErrNotExist) {
t.Errorf("invalid path must NOT be silently treated as not-exist: %v", err)
}
}
// TestCheckSidecarBehindRaftLog_NoRuntimes returns nil when the
// runtimes slice is empty or no entry matches the default group
// id. Production callers always supply at least the default
// group's runtime, but the defensive return prevents a nil-deref
// on misconfigured shard maps.
func TestCheckSidecarBehindRaftLog_NoRuntimes(t *testing.T) {
sidecarPath := writeMinimalSidecar(t, 10)
err := checkSidecarBehindRaftLog(nil, 1, sidecarPath, true)
if err != nil {
t.Fatalf("guard must skip when no runtimes match default group; got %v", err)
}
}
// TestCheckSidecarBehindRaftLog_NilEngineFailsClosed verifies
// that a present default-group runtime whose engine has not been
// constructed (nil engine field after buildShardGroups) is
// reported as an error rather than silently passing the guard.
// Rationale: at this point in startup the runtime existed but
// the engine opener failed without surfacing an error, so the
// node never finished coming up. Silently returning nil here
// would let the guard pass on a node that cannot serve, defeating
// the §9.1 fail-closed contract.
func TestCheckSidecarBehindRaftLog_NilEngineFailsClosed(t *testing.T) {
sidecarPath := writeMinimalSidecar(t, 10)
rt := &raftGroupRuntime{spec: groupSpec{id: 1}} // engine field stays nil
err := checkSidecarBehindRaftLog([]*raftGroupRuntime{rt}, 1, sidecarPath, true)
if err == nil {
t.Fatal("guard must fail-closed when default-group engine is nil")
}
if errors.Is(err, encryption.ErrSidecarBehindRaftLog) {
t.Errorf("nil-engine must NOT surface as ErrSidecarBehindRaftLog; got %v", err)
}
}
// TestRunSidecarBehindRaftLogGuard_CaughtUp pins the
// "sidecar already past engine" no-op path in the per-engine
// inner function.
func TestRunSidecarBehindRaftLogGuard_CaughtUp(t *testing.T) {
sidecarPath := writeMinimalSidecar(t, 100) // ahead of engine
gap := &stubGapEngine{
appliedIndex: 50,
scanner: &stubScanner{hit: true}, // would fire if consulted
}
err := runSidecarBehindRaftLogGuard(gap, sidecarPath, 1)
if err != nil {
t.Fatalf("guard must pass when sidecar is caught up; got %v", err)
}
}
// TestRunSidecarBehindRaftLogGuard_GapNotCovered pins the
// "behind but harmless" path.
func TestRunSidecarBehindRaftLogGuard_GapNotCovered(t *testing.T) {
sidecarPath := writeMinimalSidecar(t, 10)
gap := &stubGapEngine{
appliedIndex: 50,
scanner: &stubScanner{hit: false},
}
err := runSidecarBehindRaftLogGuard(gap, sidecarPath, 1)
if err != nil {
t.Fatalf("guard must pass when gap has no relevant entries; got %v", err)
}
}
// TestRunSidecarBehindRaftLogGuard_GapCovered pins the fire
// path: gap covers a relevant entry → ErrSidecarBehindRaftLog
// with the sidecar path + default_group annotation that
// operators see in the log line.
func TestRunSidecarBehindRaftLogGuard_GapCovered(t *testing.T) {
sidecarPath := writeMinimalSidecar(t, 10)
gap := &stubGapEngine{
appliedIndex: 50,
scanner: &stubScanner{hit: true},
}
err := runSidecarBehindRaftLogGuard(gap, sidecarPath, 1)
if !errors.Is(err, encryption.ErrSidecarBehindRaftLog) {
t.Fatalf("guard must fire ErrSidecarBehindRaftLog when gap covers a relevant entry; got %v", err)
}
}
// TestRunSidecarBehindRaftLogGuard_SidecarIndexZero_SkipsTransitionally
// pins the Stage 6C-2d skip-when-zero gate: until the §6.3 applier
// advances `sidecar.raft_applied_index` on Apply, an encrypted
// sidecar persists with index=0 and firing the guard against
// (0, engine.applied] would refuse every restart of an encrypted
// cluster on historical bootstrap/rotation entries. The guard MUST
// return nil for this transitional case even when the scanner
// would otherwise classify the range as relevant (hit=true).
func TestRunSidecarBehindRaftLogGuard_SidecarIndexZero_SkipsTransitionally(t *testing.T) {
sidecarPath := writeMinimalSidecar(t, 0) // applier-side advancement not yet shipped
gap := &stubGapEngine{
appliedIndex: 50,
scanner: &stubScanner{hit: true}, // would fire if consulted
}
err := runSidecarBehindRaftLogGuard(gap, sidecarPath, 1)
if err != nil {
t.Fatalf("guard MUST skip when sidecar.raft_applied_index=0 (applier-side advancement is a 6C-2e follow-up); got %v", err)
}
}
// TestRunSidecarBehindRaftLogGuard_ScannerError pins the
// scanner-error propagation path: the wrapped error is NOT
// marked with ErrSidecarBehindRaftLog (operator triages
// scanner failure separately).
func TestRunSidecarBehindRaftLogGuard_ScannerError(t *testing.T) {
sidecarPath := writeMinimalSidecar(t, 10)
scanErr := errors.New("simulated WAL corruption")
gap := &stubGapEngine{
appliedIndex: 50,
scanner: &stubScanner{err: scanErr},
}
err := runSidecarBehindRaftLogGuard(gap, sidecarPath, 1)
if err == nil {
t.Fatal("scanner error must propagate, got nil")
}
if errors.Is(err, encryption.ErrSidecarBehindRaftLog) {
t.Errorf("scanner error must NOT be classified as ErrSidecarBehindRaftLog; got %v", err)
}
if !errors.Is(err, scanErr) {
t.Errorf("original scanner error must be in chain; got %v", err)
}
}
// TestChainEncryptionStartupGuard_PropagatesPrevError verifies
// that a non-nil prevErr short-circuits before the guard runs.
// Pins the cyclop-reduction shape: caller is "single if err !=
// nil" downstream of the chain helper.
func TestChainEncryptionStartupGuard_PropagatesPrevError(t *testing.T) {
prev := errors.New("build failed")
got := chainEncryptionStartupGuard(prev, nil, 0, "", false)
if !errors.Is(got, prev) {
t.Fatalf("chain must propagate prev error verbatim; got %v", got)
}
}
// TestChainEncryptionStartupGuard_NilPrevRunsGuard verifies
// the other half: nil prevErr forwards to checkSidecarBehindRaftLog.
func TestChainEncryptionStartupGuard_NilPrevRunsGuard(t *testing.T) {
got := chainEncryptionStartupGuard(nil, nil, 0, "", false)
if got != nil {
t.Fatalf("chain with nil prev and skipped guard must return nil; got %v", got)
}
}