636c51e542
A customer whose machine was rebuilt had everything needed to get their data back and no way to find out: the only route was a command line. This is the screen that closes that. IT UNLOCKS, AND ONLY UNLOCKS (operator ruling). It explains, takes the recovery code, opens the repository and shows what is in there — apps, dates, sizes. It restores nothing: restore is already per-app and lives in the backups area, and a screen that unlocks and then offers to overwrite is two decisions wearing one button. ONE CORE, TWO CALLERS. RecoverInstallCore is split out of RecoverAndInstall; the CLI wrapper keeps its exit codes and printed lines byte-identical, and the handler drives the same function. Two implementations of the one operation that can permanently lose a customer's data would drift, and only one would be tested. Asserted from source on both sides by AST. THREE WAYS OUT, none a dismiss button: recover; 'most nem' (the full page stops interrupting, the backups-area entry point stays PERMANENTLY, bound to the offer and never to the postpone flag); and 'I do not want the old data' — confirmed TWICE and reaching the SHIPPED move-aside, which sets aside and never deletes. THE CODE IS HANDLED NO MORE LOOSELY THAN ON THE COMMAND LINE: POST body only, never logged, never persisted, never echoed, cleared on every path, no-store, autocomplete off. No lockout — the code is a ten-word phrase, and locking a customer out of their own data for a typo is worse than anything it prevents. TWO DEFECTS THE TESTS CAUGHT, both fixed: an UNCLAIMED (legacy-open) box would have been shown the page, because RequireAuth passes such a box through; and the inventory nil-dereferenced when no off-site target was configured, which is exactly the pristine rebuilt shape.
147 lines
4.6 KiB
Go
147 lines
4.6 KiB
Go
package web
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"testing"
|
|
)
|
|
|
|
// SCENARIO I / §8.5 — the seam-discipline tests. Both walk the AST rather than grepping, because a
|
|
// commented-out call still contains the string, and both parse with comments DROPPED so a commented
|
|
// line cannot satisfy them. This project's built-but-never-wired count is six.
|
|
|
|
// The handler must drive the SHARED core. If it ever grows its own fetch→compare→install, the CLI and
|
|
// the page can diverge and only one of them will be tested — on the one operation that can
|
|
// permanently lose a customer's data.
|
|
//
|
|
// RED-PROOF: replace the RecoverInstallCore call in recoveryUnlockHandler with an inline copy →
|
|
// this fails.
|
|
func TestRecoveryHandlerDrivesTheSharedCore(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, "recovery_handlers.go", nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse recovery_handlers.go: %v", err)
|
|
}
|
|
|
|
var fn *ast.FuncDecl
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
if d, ok := n.(*ast.FuncDecl); ok && d.Name.Name == "recoveryUnlockHandler" {
|
|
fn = d
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
if fn == nil {
|
|
t.Fatal("recoveryUnlockHandler not found — did it move? the shared-core wiring is now unasserted")
|
|
}
|
|
|
|
callsCore := false
|
|
// Any DIRECT use of the agent's unseal from the handler would be a second implementation.
|
|
callsRecoverDirectly := false
|
|
ast.Inspect(fn, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
switch sel.Sel.Name {
|
|
case "RecoverInstallCore":
|
|
callsCore = true
|
|
case "RecoverOffsiteRepoPassword", "InjectOffboxPassword":
|
|
callsRecoverDirectly = true
|
|
}
|
|
return true
|
|
})
|
|
if !callsCore {
|
|
t.Fatal("recoveryUnlockHandler does NOT call backup.RecoverInstallCore — the page and the command line would be two implementations of one irreversible operation")
|
|
}
|
|
if callsRecoverDirectly {
|
|
t.Fatal("recoveryUnlockHandler reaches the agent/injection DIRECTLY — that is a second recovery implementation, which is exactly what the shared core exists to prevent")
|
|
}
|
|
}
|
|
|
|
// And the CLI wrapper must drive the same core, from the other side.
|
|
//
|
|
// RED-PROOF: restore the inline fetch→compare→install inside RecoverAndInstall → this fails.
|
|
func TestCLIWrapperDrivesTheSharedCore(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, "../backup/offbox_recovery_cli.go", nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse offbox_recovery_cli.go: %v", err)
|
|
}
|
|
var fn *ast.FuncDecl
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
if d, ok := n.(*ast.FuncDecl); ok && d.Name.Name == "RecoverAndInstall" {
|
|
fn = d
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
if fn == nil {
|
|
t.Fatal("RecoverAndInstall not found — the shared-core wiring is now unasserted on the CLI side")
|
|
}
|
|
callsCore, callsDirect := false, false
|
|
ast.Inspect(fn, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "RecoverInstallCore" {
|
|
callsCore = true
|
|
}
|
|
if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
switch sel.Sel.Name {
|
|
case "RecoverOffsiteRepoPassword", "InjectOffboxPassword":
|
|
callsDirect = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
if !callsCore {
|
|
t.Fatal("RecoverAndInstall no longer calls RecoverInstallCore — the CLI has its own copy again")
|
|
}
|
|
if callsDirect {
|
|
t.Fatal("RecoverAndInstall reaches the agent/injection directly — the two callers have diverged")
|
|
}
|
|
}
|
|
|
|
// The page must be REACHABLE: routed in ServeHTTP, and the landing-page interception present.
|
|
//
|
|
// RED-PROOF: comment out the interception block → this fails, and a rebuilt box's owner would never
|
|
// meet the screen unless they guessed the URL.
|
|
func TestRecoveryRoutesAreWired(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, "server.go", nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse server.go: %v", err)
|
|
}
|
|
var handlersSeen, interceptSeen bool
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
switch sel.Sel.Name {
|
|
case "recoveryPageHandler", "recoveryUnlockHandler", "recoveryPostponeHandler":
|
|
handlersSeen = true
|
|
case "recoveryInterrupts":
|
|
interceptSeen = true
|
|
}
|
|
return true
|
|
})
|
|
if !handlersSeen {
|
|
t.Fatal("no recovery handler is routed in ServeHTTP — the page exists and is unreachable")
|
|
}
|
|
if !interceptSeen {
|
|
t.Fatal("ServeHTTP never consults recoveryInterrupts — the full page would never take over the landing pages, so a customer would have to guess the URL")
|
|
}
|
|
}
|