Profile Caching

Changelog-Added: Add profile caching
This commit is contained in:
William Casarin
2023-05-26 12:11:43 -07:00
11 changed files with 453 additions and 39 deletions

View File

@@ -267,6 +267,10 @@
50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; };
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; };
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; };
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */; };
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */; };
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */; };
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; };
@@ -699,6 +703,10 @@
50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; };
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; };
5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabaseTests.swift; sourceTree = "<group>"; };
501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProfile.swift; sourceTree = "<group>"; };
501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Damus.xcdatamodel; sourceTree = "<group>"; };
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; };
@@ -1006,6 +1014,7 @@
4C75EFAB28049CC80006080F /* Nostr */ = {
isa = PBXGroup;
children = (
501F8C5329FF5EE2001AFC1D /* CoreData */,
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */,
4C75EFA527FF87A20006080F /* Nostr.swift */,
4C75EFAE28049D340006080F /* NostrFilter.swift */,
@@ -1016,6 +1025,7 @@
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */,
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */,
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
4C363A8F28247A1D006E126D /* NostrLink.swift */,
50088DA029E8271A008A1FDF /* WebSocket.swift */,
@@ -1302,6 +1312,7 @@
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
@@ -1393,6 +1404,15 @@
path = Images;
sourceTree = "<group>";
};
501F8C5329FF5EE2001AFC1D /* CoreData */ = {
isa = PBXGroup;
children = (
501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */,
501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */,
);
path = CoreData;
sourceTree = "<group>";
};
7C0F392D29B57C8F0039859C /* Extensions */ = {
isa = PBXGroup;
children = (
@@ -1674,6 +1694,7 @@
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
@@ -1769,6 +1790,7 @@
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
@@ -1823,6 +1845,7 @@
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */,
@@ -1856,6 +1879,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */,
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
@@ -2375,6 +2399,19 @@
productName = secp256k1;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */,
);
currentVersion = 501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */;
path = Damus.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;
}

View File

@@ -57,7 +57,7 @@ class FollowersModel: ObservableObject {
func load_profiles(relay_id: String) {
var filter = NostrFilter.filter_profiles
let authors = find_profiles_to_fetch_pk(profiles: damus_state.profiles, event_pubkeys: contacts ?? [])
let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [])
if authors.isEmpty {
return
}

View File

@@ -24,7 +24,7 @@ class FollowingModel {
var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
// don't fetch profiles we already have
if damus_state.profiles.lookup(id: pk) != nil {
if damus_state.profiles.has_fresh_profile(id: pk) {
return
}
acc.append(pk)

View File

@@ -90,20 +90,6 @@ class SearchHomeModel: ObservableObject {
}
}
func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [String] {
var pubkeys = Set<String>()
for pk in event_pubkeys {
if profiles.lookup(id: pk) != nil {
continue
}
pubkeys.insert(pk)
}
return Array(pubkeys)
}
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] {
switch load {
case .from_events(let events):
@@ -114,17 +100,7 @@ func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: Even
}
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
var pubkeys = Set<String>()
for pk in pks {
if profiles.lookup(id: pk) != nil {
continue
}
pubkeys.insert(pk)
}
return Array(pubkeys)
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
}
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] {
@@ -132,11 +108,11 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
for ev in events {
// lookup profiles from boosted events
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), profiles.lookup(id: bev.pubkey) == nil {
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
pubkeys.insert(bev.pubkey)
}
if profiles.lookup(id: ev.pubkey) == nil {
if !profiles.has_fresh_profile(id: ev.pubkey) {
pubkeys.insert(ev.pubkey)
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="PersistedProfile" representedClassName="PersistedProfile" syncable="YES">
<attribute name="about" optional="YES" attributeType="String"/>
<attribute name="banner" optional="YES" attributeType="String"/>
<attribute name="damus_donation" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="display_name" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="last_update" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lud06" optional="YES" attributeType="String"/>
<attribute name="lud16" optional="YES" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="network_pull_date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="nip05" optional="YES" attributeType="String"/>
<attribute name="picture" optional="YES" attributeType="String"/>
<attribute name="website" optional="YES" attributeType="String"/>
</entity>
</model>

View File

@@ -0,0 +1,39 @@
//
// PersistedProfile.swift
// damus
//
// Created by Bryan Montz on 4/30/23.
//
import Foundation
import CoreData
@objc(PersistedProfile)
final class PersistedProfile: NSManagedObject {
@NSManaged var id: String?
@NSManaged var name: String?
@NSManaged var display_name: String?
@NSManaged var about: String?
@NSManaged var picture: String?
@NSManaged var banner: String?
@NSManaged var website: String?
@NSManaged var lud06: String?
@NSManaged var lud16: String?
@NSManaged var nip05: String?
@NSManaged var damus_donation: Int16
@NSManaged var last_update: Date? // The date that the profile was last updated by the user
@NSManaged var network_pull_date: Date? // The date we got this profile from a relay (for staleness checking)
func copyValues(from profile: Profile) {
name = profile.name
display_name = profile.display_name
about = profile.about
picture = profile.picture
banner = profile.banner
website = profile.website
lud06 = profile.lud06
lud16 = profile.lud16
nip05 = profile.nip05
damus_donation = profile.damus_donation != nil ? Int16(profile.damus_donation!) : 0
}
}

View File

@@ -24,6 +24,19 @@ class Profile: Codable {
self.damus_donation = damus_donation
}
convenience init(persisted_profile: PersistedProfile) {
self.init(name: persisted_profile.name,
display_name: persisted_profile.display_name,
about: persisted_profile.about,
picture: persisted_profile.picture,
banner: persisted_profile.banner,
website: persisted_profile.website,
lud06: persisted_profile.lud06,
lud16: persisted_profile.lud16,
nip05: persisted_profile.nip05,
damus_donation: Int(persisted_profile.damus_donation))
}
private func str(_ str: String) -> String? {
return get_val(str)
}

View File

@@ -0,0 +1,181 @@
//
// ProfileDatabase.swift
// damus
//
// Created by Bryan Montz on 4/30/23.
//
import Foundation
import CoreData
enum ProfileDatabaseError: Error {
case missing_context
case outdated_input
}
final class ProfileDatabase {
private let entity_name = "PersistedProfile"
private var persistent_container: NSPersistentContainer?
private var background_context: NSManagedObjectContext?
private let cache_url: URL
/// This queue is used to synchronize access to the network_pull_date_cache dictionary, which
/// prevents data races from crashing the app.
private var queue = DispatchQueue(label: "io.damus.profile_db",
qos: .userInteractive,
attributes: .concurrent)
private var network_pull_date_cache = [String: Date]()
init(cache_url: URL = ProfileDatabase.profile_cache_url) {
self.cache_url = cache_url
set_up()
}
private static var profile_cache_url: URL {
(FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("profiles"))!
}
private var persistent_store_description: NSPersistentStoreDescription {
let description = NSPersistentStoreDescription(url: cache_url)
description.type = NSSQLiteStoreType
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSSQLiteManualVacuumOption)
return description
}
private var object_model: NSManagedObjectModel? {
guard let url = Bundle.main.url(forResource: "Damus", withExtension: "momd") else {
return nil
}
return NSManagedObjectModel(contentsOf: url)
}
private func set_up() {
guard let object_model else {
print("⚠️ Warning: ProfileDatabase failed to load its object model")
return
}
persistent_container = NSPersistentContainer(name: "Damus", managedObjectModel: object_model)
persistent_container?.persistentStoreDescriptions = [persistent_store_description]
persistent_container?.loadPersistentStores { _, error in
if let error {
print("WARNING: ProfileDatabase failed to load: \(error)")
}
}
persistent_container?.viewContext.automaticallyMergesChangesFromParent = true
persistent_container?.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
background_context = persistent_container?.newBackgroundContext()
background_context?.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
}
private func get_persisted(id: String, context: NSManagedObjectContext) -> PersistedProfile? {
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
return try? context.fetch(request).first
}
func get_network_pull_date(id: String) -> Date? {
var pull_date: Date?
queue.sync {
pull_date = network_pull_date_cache[id]
}
if let pull_date {
return pull_date
}
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
request.propertiesToFetch = ["network_pull_date"]
guard let profile = try? persistent_container?.viewContext.fetch(request).first else {
return nil
}
queue.async(flags: .barrier) {
self.network_pull_date_cache[id] = profile.network_pull_date
}
return profile.network_pull_date
}
// MARK: - Public
/// Updates or inserts a new Profile into the local database. Rejects profiles whose update date
/// is older than one we already have. Database writes occur on a background context for best performance.
/// - Parameters:
/// - id: Profile id (pubkey)
/// - profile: Profile object to be stored
/// - last_update: Date that the Profile was updated
func upsert(id: String, profile: Profile, last_update: Date) async throws {
guard let context = background_context else {
throw ProfileDatabaseError.missing_context
}
try await context.perform {
var persisted_profile: PersistedProfile?
if let profile = self.get_persisted(id: id, context: context) {
if let existing_last_update = profile.last_update, last_update < existing_last_update {
throw ProfileDatabaseError.outdated_input
} else {
persisted_profile = profile
}
} else {
persisted_profile = NSEntityDescription.insertNewObject(forEntityName: self.entity_name, into: context) as? PersistedProfile
persisted_profile?.id = id
}
persisted_profile?.copyValues(from: profile)
persisted_profile?.last_update = last_update
let pull_date = Date.now
persisted_profile?.network_pull_date = pull_date
self.queue.async(flags: .barrier) {
self.network_pull_date_cache[id] = pull_date
}
try context.save()
}
}
func get(id: String) -> Profile? {
guard let container = persistent_container,
let profile = get_persisted(id: id, context: container.viewContext) else {
return nil
}
return Profile(persisted_profile: profile)
}
var count: Int {
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
let count = try? persistent_container?.viewContext.count(for: request)
return count ?? 0
}
func remove_all_profiles() throws {
guard let context = background_context, let container = persistent_container else {
throw ProfileDatabaseError.missing_context
}
queue.async(flags: .barrier) {
self.network_pull_date_cache.removeAll()
}
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entity_name)
let batch_delete_request = NSBatchDeleteRequest(fetchRequest: request)
batch_delete_request.resultType = .resultTypeObjectIDs
let result = try container.persistentStoreCoordinator.execute(batch_delete_request, with: context) as! NSBatchDeleteResult
// NSBatchDeleteRequest is an NSPersistentStoreRequest, which operates on disk. So now we'll manually update our in-memory context.
if let object_ids = result.result as? [NSManagedObjectID] {
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: object_ids
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
}
}
}

View File

@@ -6,11 +6,11 @@
//
import Foundation
import UIKit
class Profiles {
static let db_freshness_threshold: TimeInterval = 24 * 60 * 60
/// This queue is used to synchronize access to the profiles dictionary, which
/// prevents data races from crashing the app.
private var queue = DispatchQueue(label: "io.damus.profiles",
@@ -22,8 +22,10 @@ class Profiles {
var nip05_pubkey: [String: String] = [:]
var zappers: [String: String] = [:]
private let database = ProfileDatabase()
func is_validated(_ pk: String) -> NIP05? {
return validated[pk]
validated[pk]
}
func enumerated() -> EnumeratedSequence<[String: TimestampedProfile]> {
@@ -33,23 +35,29 @@ class Profiles {
}
func lookup_zapper(pubkey: String) -> String? {
if let zapper = zappers[pubkey] {
return zapper
}
return nil
zappers[pubkey]
}
func add(id: String, profile: TimestampedProfile) {
queue.async(flags: .barrier) {
self.profiles[id] = profile
}
Task {
do {
try await database.upsert(id: id, profile: profile.profile, last_update: Date(timeIntervalSince1970: TimeInterval(profile.timestamp)))
} catch {
print("⚠️ Warning: Profiles failed to save a profile: \(error)")
}
}
}
func lookup(id: String) -> Profile? {
var profile: Profile?
queue.sync {
return profiles[id]?.profile
profile = profiles[id]?.profile
}
return profile ?? database.get(id: id)
}
func lookup_with_timestamp(id: String) -> TimestampedProfile? {
@@ -57,6 +65,23 @@ class Profiles {
return profiles[id]
}
}
func has_fresh_profile(id: String) -> Bool {
// check memory first
var profile: Profile?
queue.sync {
profile = profiles[id]?.profile
}
if profile != nil {
return true
}
// then disk
guard let pull_date = database.get_network_pull_date(id: id) else {
return false
}
return Date.now.timeIntervalSince(pull_date) < Profiles.db_freshness_threshold
}
}

View File

@@ -0,0 +1,124 @@
//
// ProfileDatabaseTests.swift
// damusTests
//
// Created by Bryan Montz on 5/13/23.
//
import XCTest
@testable import damus
class ProfileDatabaseTests: XCTestCase {
static let cache_url = (FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("test-profiles"))!
let database = ProfileDatabase(cache_url: ProfileDatabaseTests.cache_url)
override func tearDownWithError() throws {
// This method is called after the invocation of each test method in the class.
try database.remove_all_profiles()
}
var test_profile: Profile {
Profile(name: "test-name",
display_name: "test-display-name",
about: "test-about",
picture: "test-picture",
banner: "test-banner",
website: "test-website",
lud06: "test-lud06",
lud16: "test-lud16",
nip05: "test-nip05",
damus_donation: 100)
}
func testStoreAndRetrieveProfile() async throws {
let id = "test-id"
let profile = test_profile
// make sure it's not there yet
XCTAssertNil(database.get(id: id))
// store the profile
try await database.upsert(id: id, profile: profile, last_update: .now)
// read the profile out of the database
let retrievedProfile = try XCTUnwrap(database.get(id: id))
XCTAssertEqual(profile.name, retrievedProfile.name)
XCTAssertEqual(profile.display_name, retrievedProfile.display_name)
XCTAssertEqual(profile.about, retrievedProfile.about)
XCTAssertEqual(profile.picture, retrievedProfile.picture)
XCTAssertEqual(profile.banner, retrievedProfile.banner)
XCTAssertEqual(profile.website, retrievedProfile.website)
XCTAssertEqual(profile.lud06, retrievedProfile.lud06)
XCTAssertEqual(profile.lud16, retrievedProfile.lud16)
XCTAssertEqual(profile.nip05, retrievedProfile.nip05)
XCTAssertEqual(profile.damus_donation, retrievedProfile.damus_donation)
}
func testRejectOutdatedProfile() async throws {
let id = "test-id"
// store a profile
let profile = test_profile
let profile_last_updated = Date.now
try await database.upsert(id: id, profile: profile, last_update: profile_last_updated)
// try to store a profile with the same id but the last_update date is older than the previously stored profile
let outdatedProfile = test_profile
let outdated_last_updated = profile_last_updated.addingTimeInterval(-60)
do {
try await database.upsert(id: id, profile: outdatedProfile, last_update: outdated_last_updated)
XCTFail("expected to throw error")
} catch let error as ProfileDatabaseError {
XCTAssertEqual(error, ProfileDatabaseError.outdated_input)
} catch {
XCTFail("not the expected error")
}
}
func testUpdateExistingProfile() async throws {
let id = "test-id"
// store a profile
let profile = test_profile
let profile_last_update = Date.now
try await database.upsert(id: id, profile: profile, last_update: profile_last_update)
// update the same profile
let updated_profile = test_profile
updated_profile.nip05 = "updated-nip05"
let updated_profile_last_update = profile_last_update.addingTimeInterval(60)
try await database.upsert(id: id, profile: updated_profile, last_update: updated_profile_last_update)
// retrieve the profile and make sure it was updated
let retrieved_profile = database.get(id: id)
XCTAssertEqual(retrieved_profile?.nip05, "updated-nip05")
}
func testStoreMultipleAndRemoveAllProfiles() async throws {
XCTAssertEqual(database.count, 0)
// store a profile
let id = "test-id"
let profile = test_profile
let profile_last_update = Date.now
try await database.upsert(id: id, profile: profile, last_update: profile_last_update)
XCTAssertEqual(database.count, 1)
// store another profile
let id2 = "test-id-2"
let profile2 = test_profile
let profile_last_update2 = Date.now
try await database.upsert(id: id2, profile: profile2, last_update: profile_last_update2)
XCTAssertEqual(database.count, 2)
try database.remove_all_profiles()
XCTAssertEqual(database.count, 0)
}
}

View File

@@ -11,7 +11,8 @@ import XCTest
final class ZapTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
let db = ProfileDatabase()
try db.remove_all_profiles()
}
override func tearDownWithError() throws {