Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Modules/Sources/Storage/GRDB/GRDBManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import GRDB

public protocol GRDBManagerProtocol {
var databaseConnection: GRDBDatabaseConnection { get }
func reset() throws
}

public protocol GRDBDatabaseConnection: DatabaseReader & DatabaseWriter {}
Expand All @@ -27,6 +28,31 @@ public final class GRDBManager: GRDBManagerProtocol {
self.databaseConnection = try DatabaseQueue()
try migrateIfNeeded()
}

/// Resets the database by deleting all data from all tables
/// Used when user logs out to ensure no data leaks between sessions
public func reset() throws {
try databaseConnection.write { db in
// Disable foreign key constraints temporarily to avoid dependency issues
try db.execute(sql: "PRAGMA foreign_keys = OFF")

// Get all user tables (excluding sqlite internal tables)
let tableNames = try String.fetchAll(db, sql: """
SELECT name FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite_%'
AND name NOT LIKE 'grdb_%'
""")

// Delete all data from each table
for tableName in tableNames {
try db.execute(sql: "DELETE FROM \(tableName)")
}

// Re-enable foreign key constraints
try db.execute(sql: "PRAGMA foreign_keys = ON")
}
}
}

private extension GRDBManager {
Expand Down
140 changes: 140 additions & 0 deletions Modules/Tests/StorageTests/GRDB/GRDBManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,146 @@ struct GRDBManagerTests {
}
}
}

struct ResetTests {
let manager: GRDBManager
let sampleSiteID: Int64 = 1

init() throws {
self.manager = try GRDBManager()
try manager.databaseConnection.write { db in
let record = TestSite(id: sampleSiteID)
try record.insert(db)
}
}

@Test("Reset clears all data from database")
func test_reset_clears_all_data_from_database() throws {
// Given - Insert comprehensive test data
try manager.databaseConnection.write { db in
// Insert second site for more comprehensive test
let site2 = TestSite(id: 2)
try site2.insert(db)

// Insert products for both sites
for siteID in [sampleSiteID, Int64(2)] {
for i in 1...2 {
let product = TestProduct(
siteID: siteID,
id: Int64(i + (siteID == 1 ? 0 : 10)),
name: "Product \(i) Site \(siteID)",
productTypeKey: "variable",
price: "\(i * 10).00",
downloadable: false,
parentID: 0,
manageStock: false,
stockStatusKey: ""
)
try product.insert(db)

// Insert variations
let variation = TestProductVariation(
siteID: siteID,
id: Int64(200 + i + (siteID == 1 ? 0 : 10)),
productID: product.id,
price: "\(i * 12).00",
downloadable: false,
manageStock: false,
stockStatusKey: ""
)
try variation.insert(db)

// Insert product attributes
let productAttribute = TestProductAttribute(
productID: product.id,
name: "Color \(i)",
position: i,
visible: true,
variation: true,
options: ["Red", "Blue", "Green"]
)
try productAttribute.insert(db)

// Insert variation attributes
let variationAttribute = TestProductVariationAttribute(
productVariationID: variation.id,
name: "Size \(i)",
option: "Large"
)
try variationAttribute.insert(db)
}
}
}

// Verify data exists before reset
let countsBefore = try manager.databaseConnection.read { db in
return (
sites: try TestSite.fetchCount(db),
products: try TestProduct.fetchCount(db),
variations: try TestProductVariation.fetchCount(db),
productAttributes: try TestProductAttribute.fetchCount(db),
variationAttributes: try TestProductVariationAttribute.fetchCount(db)
)
}

#expect(countsBefore.sites == 2) // Original site + new site
#expect(countsBefore.products == 4)
#expect(countsBefore.variations == 4)
#expect(countsBefore.productAttributes == 4)
#expect(countsBefore.variationAttributes == 4)
Comment on lines +587 to +591
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: how about checking the product/variation images as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we'll start adding every entity to this function in future, so it seems over the top to add all of them now...


// When - Reset database
try manager.reset()

// Then - All data should be cleared
let countsAfter = try manager.databaseConnection.read { db in
return (
sites: try TestSite.fetchCount(db),
products: try TestProduct.fetchCount(db),
variations: try TestProductVariation.fetchCount(db),
productAttributes: try TestProductAttribute.fetchCount(db),
variationAttributes: try TestProductVariationAttribute.fetchCount(db)
)
}

#expect(countsAfter.sites == 0)
#expect(countsAfter.products == 0)
#expect(countsAfter.variations == 0)
#expect(countsAfter.productAttributes == 0)
#expect(countsAfter.variationAttributes == 0)
}

@Test("Reset can be called multiple times without error")
func test_reset_can_be_called_multiple_times() throws {
// Given - Some test data
try manager.databaseConnection.write { db in
let product = TestProduct(
siteID: sampleSiteID,
id: 100,
name: "Test Product",
productTypeKey: "simple",
price: "10.00",
downloadable: false,
parentID: 0,
manageStock: false,
stockStatusKey: ""
)
try product.insert(db)
}

// When - Reset multiple times
try manager.reset()
try manager.reset()
try manager.reset()

// Then - Should not throw and database should be empty
let productCount = try manager.databaseConnection.read { db in
try TestProduct.fetchCount(db)
}

#expect(productCount == 0)
}
}
}

// MARK: - Test Models
Expand Down
5 changes: 5 additions & 0 deletions WooCommerce/Classes/Yosemite/DefaultStoresManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ class DefaultStoresManager: StoresManager {
ServiceLocator.analytics.refreshUserData()
ZendeskProvider.shared.reset()
ServiceLocator.storageManager.reset()
do {
try ServiceLocator.grdbManager.reset()
} catch {
DDLogError("Could not reset GRDB database: \(error)")
}
ServiceLocator.productImageUploader.reset()

updateAndReloadWidgetInformation(with: nil)
Expand Down