Skip to content

Commit

Permalink
macapp: migrate filter persisted state
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredh159 committed Oct 15, 2024
1 parent f2d8a1a commit a5f3c8b
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 12 deletions.
21 changes: 19 additions & 2 deletions macapp/App/Sources/Filter/Dependencies/FilterMigrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,29 @@ struct FilterMigrator: Migrator {
var context = "Filter"

func migrateLastVersion() async -> Persistent.State? {
await self.migrateV1()
await self.migrateV2()
}

func migrateV2() async -> Persistent.V2? {
var v1 = try? self.userDefaults.getString(Persistent.V1.storageKey).flatMap { json in
try JSON.decode(json, as: Persistent.V1.self)
}
if v1 == nil {
v1 = await self.migrateV1()
}
guard let v1 else { return nil }
log("migrating v1 state to v2")
return .init(
userKeys: v1.userKeys,
userDowntime: [:],
appIdManifest: v1.appIdManifest,
exemptUsers: v1.exemptUsers
)
}

// v1 below refers to legacy 1.x version of the app
// before ComposableArchitecture rewrite
func migrateV1() async -> Persistent.State? {
func migrateV1() async -> Persistent.V1? {
guard let exemptUserIds = userDefaults.getString("exemptUsers") else {
return nil
}
Expand Down
13 changes: 12 additions & 1 deletion macapp/App/Sources/Filter/Filter+PersitentState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ import Foundation
import Gertie

public enum Persistent {
public typealias State = V1
public typealias State = V2

// v2.5.0 - *
public struct V2: PersistentState, Sendable {
public static let version = 2
public var userKeys: [uid_t: [FilterKey]] = [:]
public var userDowntime: [uid_t: PlainTimeWindow] = [:]
public var appIdManifest = AppIdManifest()
public var exemptUsers: Set<uid_t> = []
}

// v2.0.0 - v2.4.0
public struct V1: PersistentState, Sendable {
public static let version = 1
public var userKeys: [uid_t: [FilterKey]] = [:]
Expand All @@ -17,6 +27,7 @@ extension Filter.State {
var persistent: Persistent.State {
.init(
userKeys: userKeys,
userDowntime: userDowntime,
appIdManifest: appIdManifest,
exemptUsers: exemptUsers
)
Expand Down
6 changes: 3 additions & 3 deletions macapp/App/Tests/FilterTests/EarlyDecisionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ final class EarlyDecisionTests: XCTestCase {
let date = Calendar.current.date(from: DateComponents(hour: hour, minute: minute))!
let filter = TestFilter.scenario(
userIdFromAuditToken: 502,
userDownTime: [502: downtime],
userDowntime: [502: downtime],
date: .constant(date)
)
expect(filter.earlyUserDecision(auditToken: .init())).toEqual(decision)
Expand All @@ -59,7 +59,7 @@ final class EarlyDecisionTests: XCTestCase {
let withinDowntime = Calendar.current.date(from: DateComponents(hour: 23, minute: 33))!
let filter = TestFilter.scenario(
userIdFromAuditToken: 502,
userDownTime: [502: downtime], // has downtime...
userDowntime: [502: downtime], // has downtime...
date: .constant(withinDowntime),
exemptUsers: [502] // <-- ... AND is EXEMPT, but downtime wins
)
Expand All @@ -74,7 +74,7 @@ final class EarlyDecisionTests: XCTestCase {
let withinDowntime = Calendar.current.date(from: DateComponents(hour: 23, minute: 33))!
let filter = TestFilter.scenario(
userIdFromAuditToken: 502,
userDownTime: [502: downtime], // has downtime...
userDowntime: [502: downtime], // has downtime...
date: .constant(withinDowntime),
suspensions: [502: .init( // <-- AND is SUSPENDED, but downtime wins
scope: .unrestricted,
Expand Down
56 changes: 52 additions & 4 deletions macapp/App/Tests/FilterTests/FilterMigratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,69 @@ class FilterMigratorTests: XCTestCase {
migrator.userDefaults.getString = getString.fn
let result = await migrator.migrate()
expect(result).toEqual(.mock)
expect(getString.calls).toEqual(["persistent.state.v1"])
expect(getString.calls).toEqual(["persistent.state.v2"])
}

func testMigratesV1Data() async {
func testMigratesV1ToV2() async {
var migrator = self.testMigrator

let setStringInvocations = LockIsolated<[Both<String, String>]>([])
migrator.userDefaults.setString = { key, value in
setStringInvocations.append(.init(key, value))
}

let v1Stored = Persistent.V1(
userKeys: [502: [.mock]],
appIdManifest: .init(),
exemptUsers: [503]
)

let getStringInvocations = LockIsolated<[String]>([])
migrator.userDefaults.getString = { key in
getStringInvocations.append(key)
switch key {
case "persistent.state.v2":
return nil // <-- no current state
case "persistent.state.v1":
return try! JSON.encode(v1Stored)
default:
XCTFail("Unexpected key: \(key)")
return nil
}
}

let expectedState = Persistent.State(
userKeys: v1Stored.userKeys,
userDowntime: [:], // <-- created by migrator
appIdManifest: v1Stored.appIdManifest,
exemptUsers: v1Stored.exemptUsers
)

let result = await migrator.migrate()
expect(getStringInvocations.value).toEqual(["persistent.state.v2", "persistent.state.v1"])
expect(result).toEqual(expectedState)
expect(setStringInvocations.value).toEqual([Both(
"persistent.state.v2",
try! JSON.encode(expectedState)
)])
}

func testMigratesV1Data() async {
var migrator = self.testMigrator

let setStringInvocations = LockIsolated<[Both<String, String>]>([])
migrator.userDefaults.setString = { key, value in
setStringInvocations.append(.init(key, value))
}

let getStringInvocations = LockIsolated<[String]>([])
migrator.userDefaults.getString = { key in
getStringInvocations.append(key)
switch key {
case "persistent.state.v2":
return nil // <-- no current state
case "persistent.state.v1":
return nil // <-- no v1 state
case "exemptUsers":
return "509,507"
default:
Expand All @@ -53,15 +99,17 @@ class FilterMigratorTests: XCTestCase {

let expectedState = Persistent.State(
userKeys: [:],
userDowntime: [:],
appIdManifest: .init(),
exemptUsers: [509, 507]
)

let result = await migrator.migrate()
expect(getStringInvocations.value).toEqual(["persistent.state.v1", "exemptUsers"])
expect(getStringInvocations.value)
.toEqual(["persistent.state.v2", "persistent.state.v1", "exemptUsers"])
expect(result).toEqual(expectedState)
expect(setStringInvocations.value).toEqual([Both(
"persistent.state.v1",
"persistent.state.v2",
try! JSON.encode(expectedState)
)])
}
Expand Down
9 changes: 9 additions & 0 deletions macapp/App/Tests/FilterTests/FilterReducerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ final class FilterReducerTests: XCTestCase {
let (store, _) = Filter.testStore()
store.deps.filterExtension = .mock
store.deps.storage = .mock
let save = spy(on: Persistent.State.self, returning: ())
store.deps.storage.savePersistentState = save.fn

let downtime = PlainTimeWindow(
start: .init(hour: 22, minute: 0),
Expand All @@ -131,6 +133,13 @@ final class FilterReducerTests: XCTestCase {
)))) {
$0.userDowntime[502] = downtime
}

await expect(save.calls).toEqual([.init(
userKeys: [502: [.mock]],
userDowntime: [502: downtime], // <-- new downtime info saved
appIdManifest: .mock,
exemptUsers: []
)])
}

@MainActor
Expand Down
4 changes: 2 additions & 2 deletions macapp/App/Tests/FilterTests/TestFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class TestFilter: NetworkFilter {
static func scenario(
userIdFromAuditToken userId: uid_t? = 502,
userKeys: [uid_t: [FilterKey]] = [502: [.mock]],
userDownTime: [uid_t: PlainTimeWindow] = [:],
userDowntime: [uid_t: PlainTimeWindow] = [:],
date: Dependencies.DateGenerator = .init { Date() },
appIdManifest: AppIdManifest = .init(
apps: ["chrome": ["com.chrome"]],
Expand All @@ -52,7 +52,7 @@ class TestFilter: NetworkFilter {
let filter = TestFilter()
filter.state = State(
userKeys: userKeys,
userDowntime: userDownTime,
userDowntime: userDowntime,
appIdManifest: appIdManifest,
exemptUsers: exemptUsers,
suspensions: suspensions
Expand Down

0 comments on commit a5f3c8b

Please sign in to comment.