// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB import Quick import Nimble @testable import SessionSnodeKit @testable import SessionMessagingKit @testable import SessionUtilitiesKit class DisplayPictureDownloadJobSpec: QuickSpec { override class func spec() { // MARK: Configuration @TestState var job: Job! @TestState var profile: Profile! @TestState var group: ClosedGroup! @TestState var community: OpenGroup! @TestState var dependencies: TestDependencies! = TestDependencies { dependencies in dependencies.forceSynchronous = true dependencies.dateNow = Date(timeIntervalSince1970: 1234567890) } @TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage( customWriter: try! DatabaseQueue(), migrationTargets: [ SNUtilitiesKit.self, SNMessagingKit.self ], using: dependencies, initialData: { db in try Identity(variant: .x25519PublicKey, data: Data(hex: TestConstants.publicKey)).insert(db) try Identity(variant: .x25519PrivateKey, data: Data(hex: TestConstants.privateKey)).insert(db) try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db) try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db) } ) @TestState var imageData: Data! = Data( hex: "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c" + "489000000017352474200aece1ce90000000d49444154185763f8cfc0f01f0005000" + "1ffa65c9b5d0000000049454e44ae426082" ) @TestState var encryptionKey: Data! = Data(hex: "c8e52eb1016702a663ac9a1ab5522daa128ab40762a514de271eddf598e3b8d4") @TestState var encryptedData: Data! = Data( hex: "778921bdd0e432227b53ee49c23421aeb796b7e5663468ff79daffb1af08cd1" + "a68343377fe05ab01917ce0fb8732c746a60f157f7798cdf999364b37ff9016ab2fe" + "673120e153a5cb6b869380744d493068ebc418266d6596d728cfc60b30662a089376" + "f2761e3bb6ee837a26b24b5" ) let filenameHash: String = "TestHash".bytes.toHexString() @TestState(singleton: .network, in: dependencies) var mockNetwork: MockNetwork! = MockNetwork( initialSetup: { network in network .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(MockNetwork.response(data: encryptedData)) } ) @TestState(singleton: .fileManager, in: dependencies) var mockFileManager: MockFileManager! = MockFileManager( initialSetup: { fileManager in fileManager.when { $0.fileExists(atPath: .any) }.thenReturn(false) fileManager .when { $0.fileExists(atPath: .any, isDirectory: .any) } .thenReturn(false) fileManager .when { $0.createFile(atPath: .any, contents: .any, attributes: .any) } .thenReturn(true) fileManager .when { try $0.createDirectory(atPath: .any, withIntermediateDirectories: .any, attributes: .any) } .thenReturn(()) fileManager .when { $0.containerURL(forSecurityApplicationGroupIdentifier: .any) } .thenReturn(URL(fileURLWithPath: "/test")) } ) @TestState(singleton: .crypto, in: dependencies) var mockCrypto: MockCrypto! = MockCrypto( initialSetup: { crypto in crypto.when { $0.generate(.uuid()) }.thenReturn(UUID(uuidString: "00000000-0000-0000-0000-000000001234")) crypto .when { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) } .thenReturn(imageData) crypto.when { $0.generate(.hash(message: .any, length: .any)) }.thenReturn("TestHash".bytes) crypto .when { $0.generate(.blinded15KeyPair(serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn( KeyPair( publicKey: Data(hex: TestConstants.publicKey).bytes, secretKey: Data(hex: TestConstants.edSecretKey).bytes ) ) crypto .when { $0.generate(.randomBytes(16)) } .thenReturn(Data(base64Encoded: "pK6YRtQApl4NhECGizF0Cg==")!.bytes) crypto .when { $0.generate(.signatureBlind15(message: .any, serverPublicKey: .any, ed25519SecretKey: .any)) } .thenReturn("TestSogsSignature".bytes) } ) @TestState(cache: .displayPicture, in: dependencies) var mockDisplayPictureCache: MockDisplayPictureCache! = MockDisplayPictureCache( initialSetup: { displayPictureCache in displayPictureCache.when { $0.imageData }.thenReturn([:]) displayPictureCache.when { $0.imageData = .any }.thenReturn(()) } ) // MARK: - a DisplayPictureDownloadJob describe("a DisplayPictureDownloadJob") { // MARK: -- fails when not given any details it("fails when not given any details") { job = Job(variant: .displayPictureDownload) var error: Error? = nil var permanentFailure: Bool = false DisplayPictureDownloadJob.run( job, queue: .main, success: { _, _ in }, failure: { _, runError, runPermanentFailure in error = runError permanentFailure = runPermanentFailure }, deferred: { _ in }, using: dependencies ) expect(error).to(matchError(JobRunnerError.missingRequiredDetails)) expect(permanentFailure).to(beTrue()) } // MARK: -- when initialising details context("when initialising details") { // MARK: ---- for a profile context("for a profile") { // MARK: ------ with a target context("with a target") { // MARK: -------- returns nil when given an empty url it("returns nil when given an empty url") { expect( DisplayPictureDownloadJob.Details( target: .profile(id: "", url: "", encryptionKey: Data()), timestamp: 0 ) ).to(beNil()) } // MARK: -------- returns nil when given a url which does not have a file id it("returns nil when given a url which does not have a file id") { expect( DisplayPictureDownloadJob.Details( target: .profile(id: "", url: "http://oxen.io", encryptionKey: Data()), timestamp: 0 ) ).to(beNil()) } // MARK: -------- returns nil when given a url which does not have a file id it("returns nil when given a url which does not have a file id") { expect( DisplayPictureDownloadJob.Details( target: .profile(id: "", url: "http://oxen.io", encryptionKey: Data()), timestamp: 0 ) ).to(beNil()) } // MARK: -------- returns nil when given encryption key data with the wrong length it("returns nil when given encryption key data with the wrong length") { expect( DisplayPictureDownloadJob.Details( target: .profile(id: "", url: "http://oxen.io/1234/", encryptionKey: Data([1, 2, 3])), timestamp: 0 ) ).to(beNil()) } // MARK: -------- returns a value when given valid data it("returns a value when given valid data") { expect( DisplayPictureDownloadJob.Details( target: .profile( id: "", url: "http://oxen.io/1234/", encryptionKey: encryptionKey ), timestamp: 0 ) ).toNot(beNil()) } } // MARK: ------ with an owner context("with an owner") { // MARK: -------- returns nil when given a null url it("returns nil when given a null url") { expect( DisplayPictureDownloadJob.Details( owner: .user( Profile( id: "1234", name: "test", profilePictureUrl: nil, profileEncryptionKey: encryptionKey ) ) ) ).to(beNil()) } // MARK: -------- returns nil when given a null encryption key it("returns nil when given a null encryption key") { expect( DisplayPictureDownloadJob.Details( owner: .user( Profile( id: "1234", name: "test", profilePictureUrl: "http://oxen.io/1234/", profileEncryptionKey: nil ) ) ) ).to(beNil()) } // MARK: -------- returns a value when given valid data it("returns a value when given valid data") { expect( DisplayPictureDownloadJob.Details( owner: .user( Profile( id: "1234", name: "test", profilePictureUrl: "http://oxen.io/1234/", profileEncryptionKey: encryptionKey ) ) ) ).toNot(beNil()) } } } // MARK: ---- for a group context("for a group") { // MARK: ------ with a target context("with a target") { // MARK: -------- returns nil when given an empty url it("returns nil when given an empty url") { expect( DisplayPictureDownloadJob.Details( target: .group(id: "", url: "", encryptionKey: Data()), timestamp: 0 ) ).to(beNil()) } // MARK: -------- returns nil when given a url which does not have a file id it("returns nil when given a url which does not have a file id") { expect( DisplayPictureDownloadJob.Details( target: .group(id: "", url: "http://oxen.io", encryptionKey: Data()), timestamp: 0 ) ).to(beNil()) } // MARK: -------- returns nil when given a url which does not have a file id it("returns nil when given a url which does not have a file id") { expect( DisplayPictureDownloadJob.Details( target: .group(id: "", url: "http://oxen.io", encryptionKey: Data()), timestamp: 0 ) ).to(beNil()) } // MARK: -------- returns nil when given encryption key data with the wrong length it("returns nil when given encryption key data with the wrong length") { expect( DisplayPictureDownloadJob.Details( target: .group(id: "", url: "http://oxen.io/1234/", encryptionKey: Data([1, 2, 3])), timestamp: 0 ) ).to(beNil()) } // MARK: -------- returns a value when given valid data it("returns a value when given valid data") { expect( DisplayPictureDownloadJob.Details( target: .group( id: "", url: "http://oxen.io/1234/", encryptionKey: encryptionKey ), timestamp: 0 ) ).toNot(beNil()) } } // MARK: ------ with an owner context("with an owner") { // MARK: -------- returns nil when given a null url it("returns nil when given a null url") { expect( DisplayPictureDownloadJob.Details( owner: .group( ClosedGroup( threadId: "1234", name: "test", formationTimestamp: 0, displayPictureUrl: nil, displayPictureEncryptionKey: encryptionKey, shouldPoll: nil, invited: nil ) ) ) ).to(beNil()) } // MARK: -------- returns nil when given a null encryption key it("returns nil when given a null encryption key") { expect( DisplayPictureDownloadJob.Details( owner: .group( ClosedGroup( threadId: "1234", name: "test", formationTimestamp: 0, displayPictureUrl: "http://oxen.io/1234/", displayPictureEncryptionKey: nil, shouldPoll: nil, invited: nil ) ) ) ).to(beNil()) } // MARK: -------- returns a value when given valid data it("returns a value when given valid data") { expect( DisplayPictureDownloadJob.Details( owner: .group( ClosedGroup( threadId: "1234", name: "test", formationTimestamp: 0, displayPictureUrl: "http://oxen.io/1234/", displayPictureEncryptionKey: encryptionKey, shouldPoll: nil, invited: nil ) ) ) ).toNot(beNil()) } } } // MARK: ---- for a community context("for a community") { // MARK: ------ with a target context("with a target") { // MARK: -------- returns nil when given an empty imageId it("returns nil when given an empty imageId") { expect( DisplayPictureDownloadJob.Details( target: .community(imageId: "", roomToken: "", server: ""), timestamp: 0 ) ).to(beNil()) } // MARK: -------- returns a value when given valid data it("returns a value when given valid data") { expect( DisplayPictureDownloadJob.Details( target: .community(imageId: "12", roomToken: "", server: ""), timestamp: 0 ) ).toNot(beNil()) } } // MARK: ------ with an owner context("with an owner") { // MARK: -------- returns nil when given an empty imageId it("returns nil when given an empty imageId") { expect( DisplayPictureDownloadJob.Details( owner: .community( OpenGroup( server: "testServer", roomToken: "testRoom", publicKey: "1234", isActive: false, name: "test", imageId: nil, userCount: 0, infoUpdates: 0 ) ) ) ).to(beNil()) } // MARK: -------- returns a value when given valid data it("returns a value when given valid data") { expect( DisplayPictureDownloadJob.Details( owner: .community( OpenGroup( server: "testServer", roomToken: "testRoom", publicKey: "1234", isActive: false, name: "test", imageId: "12", userCount: 0, infoUpdates: 0 ) ) ) ).toNot(beNil()) } } } } // MARK: -- when hashing context("when hashing") { // MARK: ---- generates the same hash with the same data it("generates the same hash with the same data") { expect( DisplayPictureDownloadJob.Details( target: .community(imageId: "12", roomToken: "test", server: "test"), timestamp: 1234 )?.hashValue ).to(equal( DisplayPictureDownloadJob.Details( target: .community(imageId: "12", roomToken: "test", server: "test"), timestamp: 1234 )?.hashValue )) } // MARK: ---- generates a different hash with different data it("generates a different hash with different data") { expect( DisplayPictureDownloadJob.Details( target: .community(imageId: "12", roomToken: "test", server: "test"), timestamp: 1234 )?.hashValue ).toNot(equal( DisplayPictureDownloadJob.Details( target: .community(imageId: "13", roomToken: "test", server: "test"), timestamp: 1234 )?.hashValue )) } // MARK: ---- excludes the timestamp when generating the hash value it("excludes the timestamp when generating the hash value") { expect( DisplayPictureDownloadJob.Details( target: .community(imageId: "12", roomToken: "test", server: "test"), timestamp: 1234 )?.hashValue ).to(equal( DisplayPictureDownloadJob.Details( target: .community(imageId: "12", roomToken: "test", server: "test"), timestamp: 4321 )?.hashValue )) } } // MARK: -- generates a FileServer download request correctly it("generates a FileServer download request correctly") { job = Job( variant: .displayPictureDownload, shouldBeUnique: true, details: DisplayPictureDownloadJob.Details( target: .profile( id: "", url: "http://filev2.getsession.org/file/1234", encryptionKey: encryptionKey ), timestamp: 0 ) ) let expectedRequest: Network.PreparedRequest<Data> = try Network.preparedDownload( url: URL(string: "http://filev2.getsession.org/file/1234")!, using: dependencies ) DisplayPictureDownloadJob.run( job, queue: .main, success: { _, _ in }, failure: { _, _, _ in }, deferred: { _ in }, using: dependencies ) expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( expectedRequest.body, to: expectedRequest.destination, requestTimeout: expectedRequest.requestTimeout, requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout ) }) } // MARK: -- generates a SOGS download request correctly it("generates a SOGS download request correctly") { mockStorage.write { db in try OpenGroup( server: "testServer", roomToken: "testRoom", publicKey: TestConstants.serverPublicKey, isActive: false, name: "test", userCount: 0, infoUpdates: 0 ).insert(db) } job = Job( variant: .displayPictureDownload, shouldBeUnique: true, details: DisplayPictureDownloadJob.Details( target: .community( imageId: "12", roomToken: "testRoom", server: "testServer" ), timestamp: 0 ) ) let expectedRequest: Network.PreparedRequest<Data> = mockStorage.read { db in try OpenGroupAPI.preparedDownload( db, fileId: "12", from: "testRoom", on: "testserver", using: dependencies ) }! DisplayPictureDownloadJob.run( job, queue: .main, success: { _, _ in }, failure: { _, _, _ in }, deferred: { _ in }, using: dependencies ) expect(mockNetwork) .to(call(.exactly(times: 1), matchingParameters: .all) { network in network.send( expectedRequest.body, to: expectedRequest.destination, requestTimeout: expectedRequest.requestTimeout, requestAndPathBuildTimeout: expectedRequest.requestAndPathBuildTimeout ) }) } // MARK: -- checking if a downloaded display picture is valid context("checking if a downloaded display picture is valid") { @TestState var jobResult: JobRunner.JobResult! = .notFound beforeEach { profile = Profile( id: "1234", name: "test", profilePictureUrl: nil, profilePictureFileName: nil, profileEncryptionKey: nil, lastProfilePictureUpdate: nil ) mockStorage.write { db in try profile.insert(db) } job = Job( variant: .displayPictureDownload, shouldBeUnique: true, details: DisplayPictureDownloadJob.Details( target: .profile( id: "1234", url: "http://oxen.io/100/", encryptionKey: encryptionKey ), timestamp: 1234567891 ) ) } justBeforeEach { DisplayPictureDownloadJob.run( job, queue: .main, success: { _, _ in jobResult = .succeeded }, failure: { _, error, permanent in jobResult = .failed(error, permanent) }, deferred: { _ in jobResult = .deferred }, using: dependencies ) } // MARK: ---- when it fails to decrypt the data context("when it fails to decrypt the data") { beforeEach { mockCrypto .when { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) } .thenReturn(nil) } // MARK: ------ does not save the picture it("does not save the picture") { expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try Profile.fetchOne(db) }).to(equal(profile)) } } // MARK: ---- when it decrypts invalid image data context("when it decrypts invalid image data") { beforeEach { mockCrypto .when { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) } .thenReturn(Data([1, 2, 3])) } // MARK: ------ does not save the picture it("does not save the picture") { expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try Profile.fetchOne(db) }).to(equal(profile)) } } // MARK: ---- when it fails to write to disk context("when it fails to write to disk") { beforeEach { mockFileManager .when { $0.createFile(atPath: .any, contents: .any, attributes: .any) } .thenReturn(false) } // MARK: ------ does not save the picture it("does not save the picture") { expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try Profile.fetchOne(db) }).to(equal(profile)) } } // MARK: ---- writes the file to disk it("writes the file to disk") { expect(mockFileManager) .to(call(.exactly(times: 1), matchingParameters: .all) { mockFileManager in mockFileManager.createFile( atPath: "/test/ProfileAvatars/\(filenameHash).jpg", contents: imageData, attributes: nil ) }) } // MARK: ---- adds the image data to the displayPicture cache it("adds the image data to the displayPicture cache") { expect(mockDisplayPictureCache) .to(call(.exactly(times: 1), matchingParameters: .all) { $0.imageData = ["\(filenameHash).jpg": imageData] }) } // MARK: ---- successfully completes the job it("successfully completes the job") { expect(jobResult).to(equal(.succeeded)) } // MARK: ---- for a profile context("for a profile") { beforeEach { profile = Profile( id: "1234", name: "test", profilePictureUrl: "http://oxen.io/100/", profilePictureFileName: nil, profileEncryptionKey: encryptionKey, lastProfilePictureUpdate: 1234567890 ) mockStorage.write { db in _ = try Profile.deleteAll(db) try profile.insert(db) } job = Job( variant: .displayPictureDownload, shouldBeUnique: true, details: DisplayPictureDownloadJob.Details( target: .profile( id: "1234", url: "http://oxen.io/100/", encryptionKey: encryptionKey ), timestamp: 1234567891 ) ) } // MARK: ------ that does not exist context("that does not exist") { beforeEach { mockStorage.write { db in try Profile.deleteAll(db) } } // MARK: -------- does not save the picture it("does not save the picture") { expect(mockCrypto) .toNot(call { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) }) expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try Profile.fetchOne(db) }).to(beNil()) } } // MARK: ------ that has a different encryption key and more recent update context("that has a different encryption key and more recent update") { beforeEach { mockStorage.write { db in try Profile .updateAll( db, Profile.Columns.profileEncryptionKey.set(to: Data([1, 2, 3])), Profile.Columns.lastProfilePictureUpdate.set(to: 9999999999) ) } } // MARK: -------- does not save the picture it("does not save the picture") { expect(mockCrypto) .toNot(call { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) }) expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try Profile.fetchOne(db) }) .toNot(equal( Profile( id: "1234", name: "test", profilePictureUrl: "http://oxen.io/100/", profilePictureFileName: "\(filenameHash).jpg", profileEncryptionKey: encryptionKey, lastProfilePictureUpdate: 1234567891 ) )) } } // MARK: ------ that has a different url and more recent update context("that has a different url and more recent update") { beforeEach { mockStorage.write { db in try Profile .updateAll( db, Profile.Columns.profilePictureUrl.set(to: "testUrl"), Profile.Columns.lastProfilePictureUpdate.set(to: 9999999999) ) } } // MARK: -------- does not save the picture it("does not save the picture") { expect(mockCrypto) .toNot(call { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) }) expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try Profile.fetchOne(db) }) .toNot(equal( Profile( id: "1234", name: "test", profilePictureUrl: "http://oxen.io/100/", profilePictureFileName: "\(filenameHash).jpg", profileEncryptionKey: encryptionKey, lastProfilePictureUpdate: 1234567891 ) )) } } // MARK: ------ that has a more recent update but the same url and encryption key context("that has a more recent update but the same url and encryption key") { beforeEach { mockStorage.write { db in try Profile .updateAll( db, Profile.Columns.lastProfilePictureUpdate.set(to: 9999999999) ) } } // MARK: -------- saves the picture it("saves the picture") { expect(mockCrypto) .to(call { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) }) expect(mockFileManager).to(call(.exactly(times: 1), matchingParameters: .all) { $0.createFile( atPath: "/test/ProfileAvatars/\(filenameHash).jpg", contents: imageData, attributes: nil ) }) expect(mockDisplayPictureCache).to(call(.exactly(times: 1), matchingParameters: .all) { $0.imageData = ["\(filenameHash).jpg": imageData] }) expect(mockStorage.read { db in try Profile.fetchOne(db) }) .to(equal( Profile( id: "1234", name: "test", profilePictureUrl: "http://oxen.io/100/", profilePictureFileName: "\(filenameHash).jpg", profileEncryptionKey: encryptionKey, lastProfilePictureUpdate: 1234567891 ) )) } } // MARK: ------ updates the database values it("updates the database values") { expect(mockStorage.read { db in try Profile.fetchOne(db) }) .to(equal( Profile( id: "1234", name: "test", profilePictureUrl: "http://oxen.io/100/", profilePictureFileName: "\(filenameHash).jpg", profileEncryptionKey: encryptionKey, lastProfilePictureUpdate: 1234567891 ) )) } } // MARK: ---- for a group context("for a group") { beforeEach { group = ClosedGroup( threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", name: "TestGroup", groupDescription: nil, formationTimestamp: 1234567890, displayPictureUrl: "http://oxen.io/100/", displayPictureFilename: nil, displayPictureEncryptionKey: encryptionKey, lastDisplayPictureUpdate: 1234567890, shouldPoll: true, groupIdentityPrivateKey: nil, authData: Data([1, 2, 3]), invited: false ) mockStorage.write { db in _ = try ClosedGroup.deleteAll(db) try SessionThread.fetchOrCreate( db, id: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", variant: .group, creationDateTimestamp: 1234567890, shouldBeVisible: true, calledFromConfig: nil, using: dependencies ).upsert(db) try group.insert(db) } job = Job( variant: .displayPictureDownload, shouldBeUnique: true, details: DisplayPictureDownloadJob.Details( target: .group( id: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", url: "http://oxen.io/100/", encryptionKey: encryptionKey ), timestamp: 1234567891 ) ) } // MARK: ------ that does not exist context("that does not exist") { beforeEach { mockStorage.write { db in try ClosedGroup.deleteAll(db) } } // MARK: -------- does not save the picture it("does not save the picture") { expect(mockCrypto) .toNot(call { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) }) expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }).to(beNil()) } } // MARK: ------ that has a different encryption key and more recent update context("that has a different encryption key and more recent update") { beforeEach { mockStorage.write { db in try ClosedGroup .updateAll( db, ClosedGroup.Columns.displayPictureEncryptionKey.set(to: Data([1, 2, 3])), ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999) ) } } // MARK: -------- does not save the picture it("does not save the picture") { expect(mockCrypto) .toNot(call { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) }) expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }) .toNot(equal( ClosedGroup( threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", name: "TestGroup", groupDescription: nil, formationTimestamp: 1234567890, displayPictureUrl: "http://oxen.io/100/", displayPictureFilename: "\(filenameHash).jpg", displayPictureEncryptionKey: encryptionKey, lastDisplayPictureUpdate: 1234567891, shouldPoll: true, groupIdentityPrivateKey: nil, authData: Data([1, 2, 3]), invited: false ) )) } } // MARK: ------ that has a different url and more recent update context("that has a different url and more recent update") { beforeEach { mockStorage.write { db in try ClosedGroup .updateAll( db, ClosedGroup.Columns.displayPictureUrl.set(to: "testUrl"), ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999) ) } } // MARK: -------- does not save the picture it("does not save the picture") { expect(mockCrypto) .toNot(call { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) }) expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }) .toNot(equal( ClosedGroup( threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", name: "TestGroup", groupDescription: nil, formationTimestamp: 1234567890, displayPictureUrl: "http://oxen.io/100/", displayPictureFilename: "\(filenameHash).jpg", displayPictureEncryptionKey: encryptionKey, lastDisplayPictureUpdate: 1234567891, shouldPoll: true, groupIdentityPrivateKey: nil, authData: Data([1, 2, 3]), invited: false ) )) } } // MARK: ------ that has a more recent update but the same url and encryption key context("that has a more recent update but the same url and encryption key") { beforeEach { mockStorage.write { db in try ClosedGroup .updateAll( db, ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999) ) } } // MARK: -------- saves the picture it("saves the picture") { expect(mockCrypto) .to(call { $0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any)) }) expect(mockFileManager).to(call(.exactly(times: 1), matchingParameters: .all) { $0.createFile( atPath: "/test/ProfileAvatars/\(filenameHash).jpg", contents: imageData, attributes: nil ) }) expect(mockDisplayPictureCache).to(call(.exactly(times: 1), matchingParameters: .all) { $0.imageData = ["\(filenameHash).jpg": imageData] }) expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }) .to(equal( ClosedGroup( threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", name: "TestGroup", groupDescription: nil, formationTimestamp: 1234567890, displayPictureUrl: "http://oxen.io/100/", displayPictureFilename: "\(filenameHash).jpg", displayPictureEncryptionKey: encryptionKey, lastDisplayPictureUpdate: 1234567891, shouldPoll: true, groupIdentityPrivateKey: nil, authData: Data([1, 2, 3]), invited: false ) )) } } // MARK: ------ updates the database values it("updates the database values") { expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }) .to(equal( ClosedGroup( threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", name: "TestGroup", groupDescription: nil, formationTimestamp: 1234567890, displayPictureUrl: "http://oxen.io/100/", displayPictureFilename: "\(filenameHash).jpg", displayPictureEncryptionKey: encryptionKey, lastDisplayPictureUpdate: 1234567891, shouldPoll: true, groupIdentityPrivateKey: nil, authData: Data([1, 2, 3]), invited: false ) )) } } // MARK: ---- for a community context("for a community") { beforeEach { community = OpenGroup( server: "testServer", roomToken: "testRoom", publicKey: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", isActive: true, name: "name", imageId: "100", userCount: 1, infoUpdates: 1, displayPictureFilename: nil, lastDisplayPictureUpdate: 1234567890 ) mockStorage.write { db in _ = try OpenGroup.deleteAll(db) try SessionThread.fetchOrCreate( db, id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"), variant: .community, creationDateTimestamp: 1234567890, shouldBeVisible: true, calledFromConfig: nil, using: dependencies ).upsert(db) try community.insert(db) } job = Job( variant: .displayPictureDownload, shouldBeUnique: true, details: DisplayPictureDownloadJob.Details( target: .community( imageId: "100", roomToken: "testRoom", server: "testServer" ), timestamp: 1234567891 ) ) // SOGS doesn't encrypt it's images so replace the encrypted mock response mockNetwork .when { $0.send(.any, to: .any, requestTimeout: .any, requestAndPathBuildTimeout: .any) } .thenReturn(MockNetwork.response(data: imageData)) } // MARK: ------ that does not exist context("that does not exist") { beforeEach { mockStorage.write { db in try OpenGroup.deleteAll(db) } } // MARK: -------- does not save the picture it("does not save the picture") { expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try OpenGroup.fetchOne(db) }).to(beNil()) } } // MARK: ------ that has a different imageId and more recent update context("that has a different imageId and more recent update") { beforeEach { mockStorage.write { db in try OpenGroup .updateAll( db, OpenGroup.Columns.imageId.set(to: "101"), OpenGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999) ) } } // MARK: -------- does not save the picture it("does not save the picture") { expect(mockFileManager) .toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) }) expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any }) expect(mockStorage.read { db in try OpenGroup.fetchOne(db) }) .toNot(equal( OpenGroup( server: "testServer", roomToken: "testRoom", publicKey: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", isActive: true, name: "name", imageId: "100", userCount: 1, infoUpdates: 1, displayPictureFilename: "\(filenameHash).jpg", lastDisplayPictureUpdate: 1234567891 ) )) } } // MARK: ------ that has a more recent update but the same imageId context("that has a more recent update but the same imageId") { beforeEach { mockStorage.write { db in try OpenGroup .updateAll( db, OpenGroup.Columns.imageId.set(to: "100"), OpenGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999) ) } } // MARK: -------- saves the picture it("saves the picture") { expect(mockFileManager).to(call(.exactly(times: 1), matchingParameters: .all) { $0.createFile( atPath: "/test/ProfileAvatars/\(filenameHash).jpg", contents: imageData, attributes: nil ) }) expect(mockDisplayPictureCache).to(call(.exactly(times: 1), matchingParameters: .all) { $0.imageData = ["\(filenameHash).jpg": imageData] }) expect(mockStorage.read { db in try OpenGroup.fetchOne(db) }) .to(equal( OpenGroup( server: "testServer", roomToken: "testRoom", publicKey: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", isActive: true, name: "name", imageId: "100", userCount: 1, infoUpdates: 1, displayPictureFilename: "\(filenameHash).jpg", lastDisplayPictureUpdate: 1234567891 ) )) } } // MARK: ------ updates the database values it("updates the database values") { expect(mockStorage.read { db in try OpenGroup.fetchOne(db) }) .to(equal( OpenGroup( server: "testServer", roomToken: "testRoom", publicKey: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece", isActive: true, name: "name", imageId: "100", userCount: 1, infoUpdates: 1, displayPictureFilename: "\(filenameHash).jpg", lastDisplayPictureUpdate: 1234567891 ) )) } } } } } }