package main // firmware_close_guard_test.go — M9-4.5:Wails OnBeforeClose firmware-aware 攔截邏輯測試 // // 覆蓋情境: // 1. forceCloseAccepted=true → 放行(return false)、旗標被清掉 // 2. server port=0 → 放行 // 3. queryFirmwareTasks 失敗 → fail-open 放行 // 4. hasActive=false → 放行、不 emit event // 5. hasActive=true → emit event + return true(prevent)、payload schema 正確 // 6. ConfirmForceClose → 設旗標 + 下次 evaluateClose 放行 // 7. nil deps → 防呆放行 // 8. concurrent ConfirmForceClose 與 evaluateClose → race-free import ( "context" "errors" "sync" "sync/atomic" "testing" ) // fakeCloseGuardDeps 是 CloseGuardDeps 的 test double。 type fakeCloseGuardDeps struct { mu sync.Mutex port int queryResp FirmwareActiveTasksResponse queryErr error queryCalls int emitCalls int lastEmitPayload map[string]interface{} logLines []string } func (f *fakeCloseGuardDeps) ServerPort() int { f.mu.Lock() defer f.mu.Unlock() return f.port } func (f *fakeCloseGuardDeps) QueryFirmwareTasks(ctx context.Context, port int) (FirmwareActiveTasksResponse, error) { f.mu.Lock() defer f.mu.Unlock() f.queryCalls++ return f.queryResp, f.queryErr } func (f *fakeCloseGuardDeps) EmitFirmwareInProgress(payload map[string]interface{}) { f.mu.Lock() defer f.mu.Unlock() f.emitCalls++ f.lastEmitPayload = payload } func (f *fakeCloseGuardDeps) AppLog(format string, args ...interface{}) { f.mu.Lock() defer f.mu.Unlock() f.logLines = append(f.logLines, format) } // ────────────────────────────────────────────────────────────────────── // 1. forceCloseAccepted=true → 放行、旗標清空 // ────────────────────────────────────────────────────────────────────── func TestEvaluateClose_ForceAccepted(t *testing.T) { g := NewFirmwareCloseGuard() g.ConfirmForceClose() deps := &fakeCloseGuardDeps{ port: 8080, queryResp: FirmwareActiveTasksResponse{HasActive: true}, // 即使有 active task 也該放行 } prevent := g.evaluateClose(context.Background(), deps) if prevent { t.Errorf("expected prevent=false when force accepted, got true") } // queryFirmwareTasks 不該被叫(force-close 短路) if deps.queryCalls != 0 { t.Errorf("expected queryFirmwareTasks NOT called on force accept, got %d", deps.queryCalls) } if deps.emitCalls != 0 { t.Errorf("expected no emit on force accept, got %d", deps.emitCalls) } // 旗標應該已被清掉、下次呼叫如沒 active 才放行 deps.queryResp = FirmwareActiveTasksResponse{HasActive: true, Tasks: []FirmwareActiveTaskSummary{{TaskID: "x"}}} prevent2 := g.evaluateClose(context.Background(), deps) if !prevent2 { t.Errorf("expected prevent=true on 2nd call (flag cleared), got false") } } // ────────────────────────────────────────────────────────────────────── // 2. server port<=0 → 放行 // ────────────────────────────────────────────────────────────────────── func TestEvaluateClose_ServerNotRunning(t *testing.T) { g := NewFirmwareCloseGuard() deps := &fakeCloseGuardDeps{port: 0} prevent := g.evaluateClose(context.Background(), deps) if prevent { t.Errorf("expected prevent=false when server not running") } if deps.queryCalls != 0 { t.Errorf("expected queryFirmwareTasks NOT called when port=0, got %d", deps.queryCalls) } } // ────────────────────────────────────────────────────────────────────── // 3. queryFirmwareTasks 失敗 → fail-open 放行 // ────────────────────────────────────────────────────────────────────── func TestEvaluateClose_QueryError_FailOpen(t *testing.T) { g := NewFirmwareCloseGuard() deps := &fakeCloseGuardDeps{ port: 3721, queryErr: errors.New("connection refused"), } prevent := g.evaluateClose(context.Background(), deps) if prevent { t.Errorf("expected prevent=false (fail-open) on query error, got true") } if deps.queryCalls != 1 { t.Errorf("expected queryFirmwareTasks called once, got %d", deps.queryCalls) } if deps.emitCalls != 0 { t.Errorf("expected no emit on query error, got %d", deps.emitCalls) } } // ────────────────────────────────────────────────────────────────────── // 4. hasActive=false → 放行、不 emit // ────────────────────────────────────────────────────────────────────── func TestEvaluateClose_NoActiveTask(t *testing.T) { g := NewFirmwareCloseGuard() deps := &fakeCloseGuardDeps{ port: 3721, queryResp: FirmwareActiveTasksResponse{HasActive: false, Tasks: []FirmwareActiveTaskSummary{}}, } prevent := g.evaluateClose(context.Background(), deps) if prevent { t.Errorf("expected prevent=false when no active task") } if deps.emitCalls != 0 { t.Errorf("expected no emit when no active task, got %d", deps.emitCalls) } } // ────────────────────────────────────────────────────────────────────── // 5. hasActive=true → emit event + return true、payload schema 正確 // ────────────────────────────────────────────────────────────────────── func TestEvaluateClose_HasActive_PreventAndEmit(t *testing.T) { g := NewFirmwareCloseGuard() deps := &fakeCloseGuardDeps{ port: 3721, queryResp: FirmwareActiveTasksResponse{ HasActive: true, Tasks: []FirmwareActiveTaskSummary{ { TaskID: "upgrade-KL520-0", DeviceID: "kl520-0", DeviceName: "KL520 #1", Chip: "KL520", Direction: "upgrade", Stage: "flashing", ElapsedMs: 12000, EtaSeconds: 45, }, }, }, } prevent := g.evaluateClose(context.Background(), deps) if !prevent { t.Errorf("expected prevent=true on active task") } if deps.emitCalls != 1 { t.Errorf("expected emit called once, got %d", deps.emitCalls) } // payload schema:hasActive + tasks(slice) 必須存在 if deps.lastEmitPayload["hasActive"] != true { t.Errorf("expected payload.hasActive=true, got %v", deps.lastEmitPayload["hasActive"]) } tasksAny, ok := deps.lastEmitPayload["tasks"].([]interface{}) if !ok { t.Fatalf("expected payload.tasks []interface{}, got %T", deps.lastEmitPayload["tasks"]) } if len(tasksAny) != 1 { t.Fatalf("expected 1 task in payload, got %d", len(tasksAny)) } task, ok := tasksAny[0].(map[string]interface{}) if !ok { t.Fatalf("expected task is map, got %T", tasksAny[0]) } // 重要欄位 frontend modal 會用 if task["taskId"] != "upgrade-KL520-0" { t.Errorf("expected taskId, got %v", task["taskId"]) } if task["deviceName"] != "KL520 #1" { t.Errorf("expected deviceName, got %v", task["deviceName"]) } if task["chip"] != "KL520" { t.Errorf("expected chip, got %v", task["chip"]) } if task["stage"] != "flashing" { t.Errorf("expected stage, got %v", task["stage"]) } if task["etaSeconds"] != 45 { t.Errorf("expected etaSeconds=45, got %v", task["etaSeconds"]) } } // ────────────────────────────────────────────────────────────────────── // 6. ConfirmForceClose 設旗標 // ────────────────────────────────────────────────────────────────────── func TestConfirmForceClose_SetsAndConsumesFlag(t *testing.T) { g := NewFirmwareCloseGuard() if g.consumeForceCloseAccepted() { t.Errorf("expected default flag=false") } g.ConfirmForceClose() if !g.consumeForceCloseAccepted() { t.Errorf("expected flag=true after ConfirmForceClose") } // consume 後旗標清掉 if g.consumeForceCloseAccepted() { t.Errorf("expected flag=false after consume") } } // ────────────────────────────────────────────────────────────────────── // 7. nil deps → 防呆放行 // ────────────────────────────────────────────────────────────────────── func TestEvaluateClose_NilDeps(t *testing.T) { g := NewFirmwareCloseGuard() prevent := g.evaluateClose(context.Background(), nil) if prevent { t.Errorf("expected prevent=false with nil deps (defensive)") } } // ────────────────────────────────────────────────────────────────────── // 8. concurrent ConfirmForceClose 與 evaluateClose 不 race(-race 模式) // ────────────────────────────────────────────────────────────────────── func TestConfirmForceClose_ConcurrentAccess(t *testing.T) { g := NewFirmwareCloseGuard() deps := &fakeCloseGuardDeps{port: 3721, queryResp: FirmwareActiveTasksResponse{HasActive: false}} const N = 100 var wg sync.WaitGroup var preventCount, allowCount int64 for i := 0; i < N; i++ { wg.Add(2) go func() { defer wg.Done() g.ConfirmForceClose() }() go func() { defer wg.Done() prevent := g.evaluateClose(context.Background(), deps) if prevent { atomic.AddInt64(&preventCount, 1) } else { atomic.AddInt64(&allowCount, 1) } }() } wg.Wait() // 不檢查具體 prevent/allow 數量(race condition between Confirm + Evaluate // 順序不可預期)、只驗 -race 模式沒抓到 race t.Logf("concurrent stress: prevent=%d allow=%d (race-free under -race)", preventCount, allowCount) }