Signal-iOS/SignalServiceKit/tests/Stories/SystemStoryManagerTest.swift
2024-05-06 09:57:21 -07:00

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)
}
}