433 lines
16 KiB
Swift
433 lines
16 KiB
Swift
//
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import XCTest
|
|
@testable import SignalServiceKit
|
|
|
|
class SystemStoryManagerTest: SSKBaseTest {
|
|
|
|
let timeout: TimeInterval = 5
|
|
|
|
var mockSignalService: OWSSignalServiceMock {
|
|
return signalService as! OWSSignalServiceMock
|
|
}
|
|
|
|
var scheduler: TestScheduler!
|
|
|
|
var manager: SystemStoryManager!
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
scheduler = TestScheduler()
|
|
databaseStorage.write { tx in
|
|
(DependenciesBridge.shared.registrationStateChangeManager as! RegistrationStateChangeManagerImpl).registerForTests(
|
|
localIdentifiers: .forUnitTests,
|
|
tx: tx.asV2Write
|
|
)
|
|
}
|
|
manager = SystemStoryManager(
|
|
fileSystem: OnboardingStoryManagerFilesystemMock.self,
|
|
schedulers: TestSchedulers(scheduler: scheduler),
|
|
storyMessageFactory: OnboardingStoryManagerStoryMessageFactoryMock.self
|
|
)
|
|
|
|
// Make everything sync.
|
|
scheduler.start()
|
|
}
|
|
|
|
// MARK: - Downloading
|
|
|
|
func testDownloadStory() throws {
|
|
mockSignalService.mockUrlSessionBuilder = { _, _, _ in
|
|
let mockSession = MockDownloadSession()
|
|
var dataCount = 0
|
|
mockSession.dataPromiseSource = { url in
|
|
dataCount += 1
|
|
guard dataCount <= 1 else {
|
|
XCTFail("Downloading more than once")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
if
|
|
let url = url,
|
|
url.path.hasSuffix(SystemStoryManager.Constants.manifestPath)
|
|
{
|
|
return .value(HTTPResponseImpl(
|
|
requestUrl: url,
|
|
status: 200,
|
|
headers: .init(),
|
|
bodyData: Self.manifestJSON
|
|
))
|
|
} else {
|
|
XCTFail("Got invalid download task url")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
}
|
|
var downloadCount = 0
|
|
mockSession.downloadPromiseSource = { url in
|
|
downloadCount += 1
|
|
guard downloadCount <= Self.imageNames.count else {
|
|
XCTFail("Downloading more than once")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
if let url = url {
|
|
XCTAssert(Self.imageNames
|
|
.map { $0 + SystemStoryManager.Constants.imageExtension }
|
|
.contains(url.lastPathComponent)
|
|
)
|
|
return .value(OWSUrlDownloadResponse(
|
|
task: URLSession.shared.dataTask(with: url), // doesn't matter
|
|
httpUrlResponse: HTTPURLResponse(
|
|
url: url,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil
|
|
)!,
|
|
downloadUrl: URL(fileURLWithPath: url.lastPathComponent)
|
|
))
|
|
} else {
|
|
XCTFail("No URL")
|
|
fatalError()
|
|
}
|
|
}
|
|
return mockSession
|
|
}
|
|
|
|
let downloadPromise = manager.enqueueOnboardingStoryDownload()
|
|
XCTAssertNotNil(downloadPromise.value)
|
|
}
|
|
|
|
func testDownloadStory_multipleTimes() throws {
|
|
mockSignalService.mockUrlSessionBuilder = { _, _, _ in
|
|
let mockSession = MockDownloadSession()
|
|
var dataCount = 0
|
|
mockSession.dataPromiseSource = { url in
|
|
dataCount += 1
|
|
guard dataCount <= 1 else {
|
|
XCTFail("Downloading more than once")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
if
|
|
let url = url,
|
|
url.path.hasSuffix(SystemStoryManager.Constants.manifestPath)
|
|
{
|
|
return .value(HTTPResponseImpl(
|
|
requestUrl: url,
|
|
status: 200,
|
|
headers: .init(),
|
|
bodyData: Self.manifestJSON
|
|
))
|
|
} else {
|
|
XCTFail("Got invalid download task url")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
}
|
|
var downloadCount = 0
|
|
mockSession.downloadPromiseSource = { url in
|
|
downloadCount += 1
|
|
guard downloadCount <= Self.imageNames.count else {
|
|
XCTFail("Downloading more than once")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
if let url = url {
|
|
XCTAssert(Self.imageNames
|
|
.map { $0 + SystemStoryManager.Constants.imageExtension }
|
|
.contains(url.lastPathComponent)
|
|
)
|
|
return .value(OWSUrlDownloadResponse(
|
|
task: URLSession.shared.dataTask(with: url), // doesn't matter
|
|
httpUrlResponse: HTTPURLResponse(
|
|
url: url,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil
|
|
)!,
|
|
downloadUrl: URL(fileURLWithPath: url.lastPathComponent)
|
|
))
|
|
} else {
|
|
XCTFail("No URL")
|
|
fatalError()
|
|
}
|
|
}
|
|
return mockSession
|
|
}
|
|
|
|
scheduler.stop()
|
|
|
|
let firstDownloadPromise = manager.enqueueOnboardingStoryDownload()
|
|
|
|
// before the first can fulfill, start a second
|
|
let secondDownloadPromise = manager.enqueueOnboardingStoryDownload()
|
|
|
|
scheduler.start()
|
|
|
|
XCTAssertNotNil(firstDownloadPromise.value)
|
|
XCTAssertNotNil(secondDownloadPromise.value)
|
|
|
|
// After we've fulfilled, try again, which should't redownload.
|
|
|
|
mockSignalService.mockUrlSessionBuilder = { _, _, _ in
|
|
XCTFail("Should not be issuing another network request.")
|
|
return .init()
|
|
}
|
|
|
|
let thirdDownloadPromise = manager.enqueueOnboardingStoryDownload()
|
|
|
|
XCTAssertNotNil(thirdDownloadPromise.value)
|
|
}
|
|
|
|
// MARK: - Viewed state
|
|
|
|
func testCleanUpViewedStory() throws {
|
|
mockSignalService.mockUrlSessionBuilder = { _, _, _ in
|
|
let mockSession = MockDownloadSession()
|
|
var dataCount = 0
|
|
mockSession.dataPromiseSource = { url in
|
|
dataCount += 1
|
|
guard dataCount <= 1 else {
|
|
XCTFail("Downloading more than once")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
if
|
|
let url = url,
|
|
url.path.hasSuffix(SystemStoryManager.Constants.manifestPath)
|
|
{
|
|
return .value(HTTPResponseImpl(
|
|
requestUrl: url,
|
|
status: 200,
|
|
headers: .init(),
|
|
bodyData: Self.manifestJSON
|
|
))
|
|
} else {
|
|
XCTFail("Got invalid download task url")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
}
|
|
var downloadCount = 0
|
|
mockSession.downloadPromiseSource = { url in
|
|
downloadCount += 1
|
|
guard downloadCount <= Self.imageNames.count else {
|
|
XCTFail("Downloading more than once")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
if let url = url {
|
|
XCTAssert(Self.imageNames
|
|
.map { $0 + SystemStoryManager.Constants.imageExtension }
|
|
.contains(url.lastPathComponent)
|
|
)
|
|
return .value(OWSUrlDownloadResponse(
|
|
task: URLSession.shared.dataTask(with: url), // doesn't matter
|
|
httpUrlResponse: HTTPURLResponse(
|
|
url: url,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil
|
|
)!,
|
|
downloadUrl: URL(fileURLWithPath: url.lastPathComponent)
|
|
))
|
|
} else {
|
|
XCTFail("No URL")
|
|
fatalError()
|
|
}
|
|
}
|
|
return mockSession
|
|
}
|
|
|
|
let downloadPromise = manager.enqueueOnboardingStoryDownload()
|
|
XCTAssertNotNil(downloadPromise.value)
|
|
|
|
// Mark all the stories viewed.
|
|
let viewedDate = Date().addingTimeInterval(-SystemStoryManager.Constants.postViewingTimeout)
|
|
write { transaction in
|
|
let stories = StoryFinder.storiesForListView(transaction: transaction)
|
|
XCTAssertEqual(stories.count, Self.imageNames.count)
|
|
stories.forEach { story in
|
|
story.markAsViewed(
|
|
at: viewedDate.ows_millisecondsSince1970,
|
|
circumstance: .onThisDevice,
|
|
transaction: transaction
|
|
)
|
|
}
|
|
}
|
|
|
|
try write {
|
|
try manager.setHasViewedOnboardingStory(
|
|
source: .local(
|
|
timestamp: viewedDate.ows_millisecondsSince1970,
|
|
shouldUpdateStorageService: false
|
|
),
|
|
transaction: $0
|
|
)
|
|
}
|
|
|
|
let cleanupPromise = manager.cleanUpOnboardingStoryIfNeeded()
|
|
XCTAssertNotNil(cleanupPromise.value)
|
|
|
|
// Check that stories were indeed deleted.
|
|
read { transaction in
|
|
let stories = StoryFinder.storiesForListView(transaction: transaction)
|
|
XCTAssert(stories.isEmpty)
|
|
}
|
|
}
|
|
|
|
func testLegacyClientDownloadedButUnviewed() throws {
|
|
// Legacy clients might have downloaded the onboarding story, but not kept track
|
|
// of its viewed state separate from the viewed timestamp on the story messages themselves.
|
|
// Force getting into this state by setting download state as downloaded but not creating
|
|
// any stories or marking viewed state, and check that we clean up and mark viewed.
|
|
|
|
// NOTE: if this test ever becomes a nuisance, its okay to delete it. This was written on
|
|
// Oct 5 2022, and only internal clients had the ability to download the onboarding
|
|
// story in this legacy state. Dropping support for those old internal clients is fine eventually.
|
|
try write {
|
|
try manager.markOnboardingStoryDownloaded(messageUniqueIds: ["1234"], transaction: $0)
|
|
}
|
|
|
|
// Triggering a download should do the cleanup.
|
|
let cleanupPromise = manager.enqueueOnboardingStoryDownload()
|
|
XCTAssertNotNil(cleanupPromise.value)
|
|
|
|
read { transaction in
|
|
if let mockManager = Self.systemStoryManager as? SystemStoryManagerMock {
|
|
mockManager.areSystemStoriesHidden = manager.areSystemStoriesHidden(transaction: transaction)
|
|
mockManager.isOnboardingStoryRead = manager.isOnboardingStoryViewed(transaction: transaction)
|
|
}
|
|
|
|
let stories = StoryFinder.unviewedSenderCount(transaction: transaction)
|
|
XCTAssert(stories == 0)
|
|
}
|
|
}
|
|
|
|
func testCleanUpViewedStory_notTimedOut() throws {
|
|
mockSignalService.mockUrlSessionBuilder = { _, _, _ in
|
|
let mockSession = MockDownloadSession()
|
|
var dataCount = 0
|
|
mockSession.dataPromiseSource = { url in
|
|
dataCount += 1
|
|
guard dataCount <= 1 else {
|
|
XCTFail("Downloading more than once")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
if
|
|
let url = url,
|
|
url.path.hasSuffix(SystemStoryManager.Constants.manifestPath)
|
|
{
|
|
return .value(HTTPResponseImpl(
|
|
requestUrl: url,
|
|
status: 200,
|
|
headers: .init(),
|
|
bodyData: Self.manifestJSON
|
|
))
|
|
} else {
|
|
XCTFail("Got invalid download task url")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
}
|
|
var downloadCount = 0
|
|
mockSession.downloadPromiseSource = { url in
|
|
downloadCount += 1
|
|
guard downloadCount <= Self.imageNames.count else {
|
|
XCTFail("Downloading more than once")
|
|
return .init(error: OWSAssertionError(""))
|
|
}
|
|
if let url = url {
|
|
XCTAssert(Self.imageNames
|
|
.map { $0 + SystemStoryManager.Constants.imageExtension }
|
|
.contains(url.lastPathComponent)
|
|
)
|
|
return .value(OWSUrlDownloadResponse(
|
|
task: URLSession.shared.dataTask(with: url), // doesn't matter
|
|
httpUrlResponse: HTTPURLResponse(
|
|
url: url,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil
|
|
)!,
|
|
downloadUrl: URL(fileURLWithPath: url.lastPathComponent)
|
|
))
|
|
} else {
|
|
XCTFail("No URL")
|
|
fatalError()
|
|
}
|
|
}
|
|
return mockSession
|
|
}
|
|
|
|
let downloadPromise = manager.enqueueOnboardingStoryDownload()
|
|
XCTAssertNotNil(downloadPromise.value)
|
|
|
|
// Mark all the stories viewed, but recently so they aren't timed out.
|
|
let viewedDate = Date()
|
|
write { transaction in
|
|
let stories = StoryFinder.storiesForListView(transaction: transaction)
|
|
XCTAssertEqual(stories.count, Self.imageNames.count)
|
|
stories.forEach { story in
|
|
story.markAsViewed(
|
|
at: viewedDate.ows_millisecondsSince1970,
|
|
circumstance: .onThisDevice,
|
|
transaction: transaction
|
|
)
|
|
}
|
|
}
|
|
|
|
let cleanupPromise = manager.cleanUpOnboardingStoryIfNeeded()
|
|
XCTAssertNotNil(cleanupPromise.value)
|
|
|
|
// Check that stories were not deleted.
|
|
read { transaction in
|
|
let stories = StoryFinder.storiesForListView(transaction: transaction)
|
|
XCTAssertEqual(stories.count, Self.imageNames.count)
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
static let imageNames = ["abc", "xyz"]
|
|
|
|
static var manifestJSON: Data {
|
|
let imageNamesString = "[\(imageNames.map({ "\"\($0)\""}).joined(separator: ","))]"
|
|
let string = """
|
|
{
|
|
"\(SystemStoryManager.Constants.manifestVersionKey)": "1234",
|
|
"\(SystemStoryManager.Constants.manifestLanguagesKey)": {
|
|
"\(Locale.current.languageCode!)": \(imageNamesString),
|
|
"anImpossibleLanguageCode": [
|
|
"fail"
|
|
]
|
|
}
|
|
}
|
|
"""
|
|
return string.data(using: .utf8)!
|
|
}
|
|
}
|
|
|
|
private class MockDownloadSession: BaseOWSURLSessionMock {
|
|
|
|
var dataPromiseSource: ((URL?) -> Promise<HTTPResponse>)?
|
|
|
|
override func dataTaskPromise(
|
|
request: URLRequest,
|
|
ignoreAppExpiry: Bool = false
|
|
) -> Promise<HTTPResponse> {
|
|
guard let dataPromiseSource = dataPromiseSource else {
|
|
fatalError()
|
|
}
|
|
|
|
return dataPromiseSource(request.url)
|
|
}
|
|
|
|
var downloadPromiseSource: ((URL?) -> Promise<OWSUrlDownloadResponse>)?
|
|
|
|
override func downloadTaskPromise(
|
|
request: URLRequest,
|
|
progress progressBlock: BaseOWSURLSessionMock.ProgressBlock?
|
|
) -> Promise<OWSUrlDownloadResponse> {
|
|
guard let downloadPromiseSource = downloadPromiseSource else {
|
|
fatalError()
|
|
}
|
|
|
|
return downloadPromiseSource(request.url)
|
|
}
|
|
}
|