feat(sdk): OTA 升级流程对齐 expo + 拆 extension 文件分层 + 秘钥本地化
主要改动: - 新增 fetchLatestFirmware/upgradeFirmware/tryReportPendingUpgrade 三段 OTA 接口,对齐 expo bindDeviceWithOrchestration 行为 - bind 仅做硬绑+getVersion,软绑/对账上报/拉最新固件搬到 fetchLatestFirmware 内部串;软绑失败自动硬解回滚 - UpgradeRecord 复合主键 (sn, firmwareId) 持久化 + sn 级 in-flight 互斥 - DuooomiBleSDKDelegate 新增 didUpdateUpgradeRecords,集成方 可观察对账上报结果(reported/outcome 状态) - DuooomiBleSDK 主类按职责拆 5 个 extension 文件,公开 API 不变 - TargetFirmware 简化为只取必要字段 + 新增 forceUpgrade - scanNamePrefix 改为必填 String - demo 秘钥移至本地 DemoSecrets.swift (gitignored) - README 重写:完整 OTA 流程示例 + 上报状态判定表 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
168
Tests/DuooomiBleSDKTests/UpgradeRecordStoreTests.swift
Normal file
168
Tests/DuooomiBleSDKTests/UpgradeRecordStoreTests.swift
Normal file
@@ -0,0 +1,168 @@
|
||||
import XCTest
|
||||
@testable import DuooomiBleSDK
|
||||
|
||||
/// 与 RN `bleStore.upgradeRecords` 行为对齐的关键回归测试。
|
||||
final class UpgradeRecordStoreTests: XCTestCase {
|
||||
|
||||
private var defaults: UserDefaults!
|
||||
private let suiteName = "duooomi.test.upgradeRecord"
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
defaults = UserDefaults(suiteName: suiteName)
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
defaults = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
private func makeStore() -> UpgradeRecordStore {
|
||||
UpgradeRecordStore(defaults: defaults)
|
||||
}
|
||||
|
||||
private func sample(sn: String = "A123456", firmwareId: String = "fw-1", triggeredAt: Int64? = nil) -> UpgradeRecord {
|
||||
UpgradeRecord(
|
||||
sn: sn,
|
||||
firmwareId: firmwareId,
|
||||
fromVersion: "1.0.0",
|
||||
targetVersion: "1.1.0",
|
||||
triggeredAt: triggeredAt ?? UpgradeRecordStore.nowMillis()
|
||||
)
|
||||
}
|
||||
|
||||
private func wait(_ s: TimeInterval = 0.1) {
|
||||
let exp = expectation(description: "delay")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + s) { exp.fulfill() }
|
||||
wait(for: [exp], timeout: s + 1)
|
||||
}
|
||||
|
||||
// MARK: - upsert 复合主键
|
||||
|
||||
func testUpsertAddsNewRecord() {
|
||||
let store = makeStore()
|
||||
store.upsert(sample())
|
||||
wait()
|
||||
XCTAssertEqual(store.all.count, 1)
|
||||
XCTAssertEqual(store.all.first?.firmwareId, "fw-1")
|
||||
}
|
||||
|
||||
func testUpsertSameKeyReplacesAndResetsReportedFlag() {
|
||||
let store = makeStore()
|
||||
let r1 = UpgradeRecord(
|
||||
sn: "A1", firmwareId: "fw-1",
|
||||
fromVersion: "1.0.0", targetVersion: "1.1.0",
|
||||
triggeredAt: 1000, reported: true, reportedAt: 2000, outcome: .success
|
||||
)
|
||||
store.upsert(r1)
|
||||
wait()
|
||||
|
||||
// 同 (sn, firmwareId),新 record 覆盖且 reported=false(重试场景)
|
||||
let r2 = UpgradeRecord(
|
||||
sn: "A1", firmwareId: "fw-1",
|
||||
fromVersion: "1.0.0", targetVersion: "1.2.0",
|
||||
triggeredAt: 5000
|
||||
)
|
||||
store.upsert(r2)
|
||||
wait()
|
||||
|
||||
XCTAssertEqual(store.all.count, 1)
|
||||
let only = store.all[0]
|
||||
XCTAssertEqual(only.targetVersion, "1.2.0")
|
||||
XCTAssertFalse(only.reported)
|
||||
XCTAssertNil(only.reportedAt)
|
||||
XCTAssertNil(only.outcome)
|
||||
}
|
||||
|
||||
func testUpsertDifferentFirmwareIdAppends() {
|
||||
let store = makeStore()
|
||||
store.upsert(sample(firmwareId: "fw-1"))
|
||||
store.upsert(sample(firmwareId: "fw-2"))
|
||||
wait()
|
||||
XCTAssertEqual(store.all.count, 2)
|
||||
}
|
||||
|
||||
func testUpsertEmptySnIgnored() {
|
||||
let store = makeStore()
|
||||
store.upsert(sample(sn: ""))
|
||||
wait()
|
||||
XCTAssertEqual(store.all.count, 0)
|
||||
}
|
||||
|
||||
// MARK: - pending 排序
|
||||
|
||||
func testPendingExcludesReportedAndSortsAsc() {
|
||||
let store = makeStore()
|
||||
store.upsert(UpgradeRecord(sn: "A", firmwareId: "fw-old", fromVersion: "1.0", targetVersion: "1.1", triggeredAt: 100))
|
||||
store.upsert(UpgradeRecord(sn: "A", firmwareId: "fw-new", fromVersion: "1.0", targetVersion: "1.2", triggeredAt: 300))
|
||||
store.upsert(UpgradeRecord(sn: "A", firmwareId: "fw-mid", fromVersion: "1.0", targetVersion: "1.15", triggeredAt: 200))
|
||||
store.upsert(UpgradeRecord(sn: "B", firmwareId: "fw-other", fromVersion: "1.0", targetVersion: "1.1", triggeredAt: 50))
|
||||
wait()
|
||||
|
||||
let pending = store.pending(sn: "A")
|
||||
XCTAssertEqual(pending.map { $0.firmwareId }, ["fw-old", "fw-mid", "fw-new"])
|
||||
|
||||
store.markReported(sn: "A", firmwareId: "fw-old", outcome: .success)
|
||||
wait()
|
||||
XCTAssertEqual(store.pending(sn: "A").map { $0.firmwareId }, ["fw-mid", "fw-new"])
|
||||
}
|
||||
|
||||
// MARK: - markReported
|
||||
|
||||
func testMarkReportedSetsFlagAndOutcome() {
|
||||
let store = makeStore()
|
||||
store.upsert(sample())
|
||||
wait()
|
||||
store.markReported(sn: "A123456", firmwareId: "fw-1", outcome: .versionMismatch)
|
||||
wait()
|
||||
|
||||
let r = store.all[0]
|
||||
XCTAssertTrue(r.reported)
|
||||
XCTAssertNotNil(r.reportedAt)
|
||||
XCTAssertEqual(r.outcome, .versionMismatch)
|
||||
}
|
||||
|
||||
func testMarkReportedTtlExpiredOutcomeNil() {
|
||||
// RN 行为:TTL 过期 → markReported(null),保留为审计轨迹
|
||||
let store = makeStore()
|
||||
store.upsert(sample())
|
||||
wait()
|
||||
store.markReported(sn: "A123456", firmwareId: "fw-1", outcome: nil)
|
||||
wait()
|
||||
let r = store.all[0]
|
||||
XCTAssertTrue(r.reported)
|
||||
XCTAssertNil(r.outcome)
|
||||
}
|
||||
|
||||
// MARK: - sn 互斥
|
||||
|
||||
func testTryAcquireReturnsFalseWhileHeld() {
|
||||
let store = makeStore()
|
||||
XCTAssertTrue(store.tryAcquire(sn: "A1"))
|
||||
XCTAssertFalse(store.tryAcquire(sn: "A1"))
|
||||
// 不同 sn 互不影响
|
||||
XCTAssertTrue(store.tryAcquire(sn: "B1"))
|
||||
}
|
||||
|
||||
func testReleaseAllowsReacquire() {
|
||||
let store = makeStore()
|
||||
XCTAssertTrue(store.tryAcquire(sn: "A1"))
|
||||
store.release(sn: "A1")
|
||||
wait()
|
||||
XCTAssertTrue(store.tryAcquire(sn: "A1"))
|
||||
}
|
||||
|
||||
// MARK: - 持久化 round-trip
|
||||
|
||||
func testRecordsPersistAcrossInstances() {
|
||||
let store1 = makeStore()
|
||||
store1.upsert(sample())
|
||||
wait()
|
||||
|
||||
let store2 = UpgradeRecordStore(defaults: defaults)
|
||||
XCTAssertEqual(store2.all.count, 1)
|
||||
XCTAssertEqual(store2.all[0].firmwareId, "fw-1")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user