Dactyloidae iOS initial commit

This commit is contained in:
wuggy 2026-06-26 21:04:09 -07:00
commit 7154a0497e
2123 changed files with 197052 additions and 0 deletions

View file

@ -0,0 +1,284 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import UIKit
import XCTest
@testable import Client
import Shared
import Storage
import Deferred
import SyncTelemetry
class ActivityStreamTests: XCTestCase {
var profile: MockProfile!
var panel: ActivityStreamPanel!
var mockPingClient: MockPingClient!
var telemetry: ActivityStreamTracker!
override func setUp() {
super.setUp()
self.profile = MockProfile()
self.telemetry = ActivityStreamTracker(eventsTracker: MockPingClient(), sessionsTracker: MockPingClient())
self.panel = ActivityStreamPanel(profile: profile, telemetry: self.telemetry)
}
override func tearDown() {
mockPingClient = nil
}
func testDeletionOfSingleSuggestedSite() {
let siteToDelete = panel.defaultTopSites()[0]
panel.hideURLFromTopSites(siteToDelete)
let newSites = panel.defaultTopSites()
XCTAssertFalse(newSites.contains(siteToDelete, f: { (a, b) -> Bool in
return a.url == b.url
}))
}
func testDeletionOfAllDefaultSites() {
let defaultSites = panel.defaultTopSites()
defaultSites.forEach({
panel.hideURLFromTopSites($0)
})
let newSites = panel.defaultTopSites()
XCTAssertTrue(newSites.isEmpty)
}
}
// MARK: Telemetry Tests
extension ActivityStreamTests {
fileprivate func expectedPayloadForEvent(_ event: String, source: String, position: Int) -> [String: Any] {
return [
"event": event,
"page": "NEW_TAB",
"source": source,
"action_position": position,
"app_version": AppInfo.appVersion,
"build": AppInfo.buildNumber,
"locale": Locale.current.identifier,
"release_channel": AppConstants.BuildChannel.rawValue
]
}
fileprivate func expectedBadStatePayload(state: String, source: String) -> [String: Any] {
return [
"event": state,
"page": "NEW_TAB",
"source": source,
"app_version": AppInfo.appVersion,
"build": AppInfo.buildNumber,
"locale": Locale.current.identifier,
"release_channel": AppConstants.BuildChannel.rawValue
]
}
fileprivate func assertPayload(_ actual: [String: Any], matches: [String: Any]) {
XCTAssertTrue(actual.count == matches.count)
actual.enumerated().forEach { index, element in
if let actualValue = element.1 as? Int,
let matchesValue = matches[element.0] as? Int {
XCTAssertTrue(actualValue == matchesValue)
}
if let actualValue = element.1 as? String,
let matchesValue = matches[element.0] as? String {
XCTAssertTrue(actualValue == matchesValue)
}
}
}
func testHighlightEmitsEventOnTap() {
let mockSite = Site(url: "http://mozilla.org", title: "Mozilla")
panel.highlights = [mockSite]
panel.selectItemAtIndex(0, inSection: .highlights)
let pingsSent = (telemetry.eventsTracker as! MockPingClient).pingsReceived
XCTAssertEqual(pingsSent.count, 1)
let eventPing = pingsSent[0]
assertPayload(eventPing, matches: expectedPayloadForEvent("CLICK", source: "HIGHLIGHTS", position: 0))
}
func testContextMenuOnTopSiteEmitsRemoveEvent() {
let mockSite = Site(url: "http://mozilla.org", title: "Mozilla")
let topSitesContextMenu = panel.contextMenu(for: mockSite, with: IndexPath(item: 0, section: ActivityStreamPanel.Section.topSites.rawValue))
let removeAction = topSitesContextMenu?.actions[0].find { $0.title == Strings.RemoveContextMenuTitle }
removeAction?.handler?(removeAction!)
let pingsSent = (telemetry.eventsTracker as! MockPingClient).pingsReceived
XCTAssertEqual(pingsSent.count, 1)
let removePing = pingsSent[0]
assertPayload(removePing, matches: expectedPayloadForEvent("REMOVE", source: "TOP_SITES", position: 0))
}
func testContextMenuOnHighlightsEmitsRemoveDismissEvents() {
let mockSite = Site(url: "http://mozilla.org", title: "Mozilla")
let highlightsContextMenu = panel.contextMenu(for: mockSite, with: IndexPath(row: 0, section: ActivityStreamPanel.Section.highlights.rawValue))
let dismiss = highlightsContextMenu?.actions[0].find { $0.title == Strings.RemoveContextMenuTitle }
let delete = highlightsContextMenu?.actions[0].find { $0.title == Strings.DeleteFromHistoryContextMenuTitle }
dismiss?.handler?(dismiss!)
delete?.handler?(delete!)
// Check to see that they emitted telemetry events
let pingsSent = (telemetry.eventsTracker as! MockPingClient).pingsReceived
XCTAssertEqual(pingsSent.count, 2)
let dismissPing = pingsSent[0]
assertPayload(dismissPing, matches: expectedPayloadForEvent("DISMISS", source: "HIGHLIGHTS", position: 0))
let deletePing = pingsSent[1]
assertPayload(deletePing, matches: expectedPayloadForEvent("DELETE", source: "HIGHLIGHTS", position: 0))
}
func testSessionReportedWhenViewAppearsAndDisappears() {
// Simulate the panel opening and closing with a second in between for some session_duration
panel.viewWillAppear(false)
var pingsSent = (telemetry.sessionsTracker as! MockPingClient).pingsReceived
XCTAssertEqual(pingsSent.count, 0)
wait(1)
panel.viewDidDisappear(false)
pingsSent = (telemetry.sessionsTracker as! MockPingClient).pingsReceived
XCTAssertEqual(pingsSent.count, 1)
let eventPing = pingsSent[0]
XCTAssertNotNil(eventPing["session_duration"])
}
func testBadStateEventsForHighlights() {
let goodSite = Site(url: "http://mozilla.org", title: "Mozilla")
goodSite.icon = Favicon(url: "http://image", date: Date(), type: .local)
goodSite.metadata = PageMetadata(id: nil,
siteURL: "http://mozilla.org",
mediaURL: "http://image",
title: "Mozilla",
description: "Web",
type: nil,
providerName: nil,
mediaDataURI: nil)
let badSite = Site(url: "http://mozilla.org", title: "Mozilla")
profile.recommendations = MockRecommender(highlights: [goodSite, badSite])
// Since invalidateHighlights calls back into the main thread, we can't
// simply call .value on this to block since the app will dead lock when
// trying to call back onto a blocked main thread.
let expect = XCTestExpectation(description: "Sent bad highlight pings")
panel.getHighlights() >>> {
expect.fulfill()
}
wait(for: [expect], timeout: 3)
let pingsSent = (self.telemetry.eventsTracker as! MockPingClient).pingsReceived
XCTAssertEqual(pingsSent.count, 2)
assertPayload(pingsSent[0],
matches: expectedBadStatePayload(state: "MISSING_METADATA_IMAGE", source: "HIGHLIGHTS"))
assertPayload(pingsSent[1],
matches: expectedBadStatePayload(state: "MISSING_FAVICON", source: "HIGHLIGHTS"))
}
func testBadStateEventsForTopSites() {
let goodSite = Site(url: "http://mozilla.org", title: "Mozilla")
goodSite.icon = Favicon(url: "http://image", date: Date(), type: .local)
goodSite.metadata = PageMetadata(id: nil,
siteURL: "http://mozilla.org",
mediaURL: "http://image",
title: "Mozilla",
description: "Web",
type: nil,
providerName: nil,
mediaDataURI: nil)
let badSite = Site(url: "http://mozilla.org", title: "Mozilla")
profile.history = MockTopSitesHistory(sites: [goodSite, badSite])
// Since invalidateHighlights calls back into the main thread, we can't
// simply call .value on this to block since the app will dead lock when
// trying to call back onto a blocked main thread.
let expect = XCTestExpectation(description: "Sent bad top site pings")
panel.getTopSites() >>> {
expect.fulfill()
}
wait(for: [expect], timeout: 3)
let pingsSent = (self.telemetry.eventsTracker as! MockPingClient).pingsReceived
XCTAssertEqual(pingsSent.count, 2)
assertPayload(pingsSent[0],
matches: expectedBadStatePayload(state: "MISSING_METADATA_IMAGE", source: "TOP_SITES"))
assertPayload(pingsSent[1],
matches: expectedBadStatePayload(state: "MISSING_FAVICON", source: "TOP_SITES"))
}
}
class MockPingClient: PingCentreClient {
var pingsReceived: [[String: Any]] = []
public func sendPing(_ data: [String : Any], validate: Bool) -> Success {
pingsReceived.append(data)
return succeed()
}
public func sendBatch(_ data: [[String : Any]], validate: Bool) -> Success {
pingsReceived += data
return succeed()
}
}
fileprivate class MockRecommender: HistoryRecommendations {
func repopulateHighlights() -> Success {
return succeed()
}
var highlights: [Site]
init(highlights: [Site]) {
self.highlights = highlights
}
func getHighlights() -> Deferred<Maybe<Cursor<Site>>> {
return deferMaybe(ArrayCursor(data: highlights))
}
func getRecentBookmarks(_ limit: Int) -> Deferred<Maybe<Cursor<Site>>> {
return deferMaybe(ArrayCursor(data: []))
}
func repopulate(invalidateTopSites shouldInvalidateTopSites: Bool, invalidateHighlights shouldInvalidateHighlights: Bool) -> Success {
return succeed()
}
func removeHighlightForURL(_ url: String) -> Success {
guard let foundSite = highlights.filter({ $0.url == url }).first else {
return succeed()
}
let foundIndex = highlights.index(of: foundSite)!
highlights.remove(at: foundIndex)
return succeed()
}
}
fileprivate class MockTopSitesHistory: MockableHistory {
let mockTopSites: [Site]
init(sites: [Site]) {
mockTopSites = sites
}
override func getTopSitesWithLimit(_ limit: Int) -> Deferred<Maybe<Cursor<Site>>> {
return deferMaybe(ArrayCursor(data: mockTopSites))
}
override func getPinnedTopSites() -> Deferred<Maybe<Cursor<Site>>> {
return deferMaybe(ArrayCursor(data: []))
}
override func updateTopSitesCacheIfInvalidated() -> Deferred<Maybe<Bool>> {
return deferMaybe(true)
}
}

View file

@ -0,0 +1,173 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import XCTest
import Shared
import Deferred
@testable import Client
@testable import Storage
class MockFiles: FileAccessor {
init() {
let docPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]
super.init(rootPath: (docPath as NSString).appendingPathComponent("testing"))
}
}
class MockChallengeSender: NSObject, URLAuthenticationChallengeSender {
func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {}
func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {}
func cancel(_ challenge: URLAuthenticationChallenge) {}
}
class MockMalformableLogin: LoginData {
var guid: String
var credentials: URLCredential
var protectionSpace: URLProtectionSpace
var hostname: String
var username: String?
var password: String
var httpRealm: String?
var formSubmitURL: String?
var usernameField: String?
var passwordField: String?
var hasMalformedHostname = true
var isValid = Maybe(success: ())
static func createWithHostname(_ hostname: String, username: String, password: String, formSubmitURL: String) -> MockMalformableLogin {
return self.init(guid: Bytes.generateGUID(), hostname: hostname, username: username, password: password, formSubmitURL: formSubmitURL)
}
required init(guid: String, hostname: String, username: String, password: String, formSubmitURL: String) {
self.guid = guid
self.credentials = URLCredential(user: username, password: password, persistence: URLCredential.Persistence.none)
self.hostname = hostname
self.password = password
self.username = username
self.protectionSpace = URLProtectionSpace(host: hostname, port: 0, protocol: nil, realm: nil, authenticationMethod: nil)
self.formSubmitURL = formSubmitURL
}
func toDict() -> [String: String] {
// Not used for this mock
return [String: String]()
}
func isSignificantlyDifferentFrom(_ login: LoginData) -> Bool {
// Not used for this mock
return true
}
}
private let MainLoginColumns = "guid, username, password, hostname, httpRealm, formSubmitURL, usernameField, passwordField"
class AuthenticatorTests: XCTestCase {
fileprivate var db: BrowserDB!
fileprivate var logins: SQLiteLogins!
fileprivate var mockVC = UIViewController()
override func setUp() {
super.setUp()
self.db = BrowserDB(filename: "testsqlitelogins.db", schema: LoginsSchema(), files: MockFiles())
self.logins = SQLiteLogins(db: self.db)
self.logins.removeAll().succeeded()
}
override func tearDown() {
self.logins.removeAll().succeeded()
}
fileprivate func mockChallengeForURL(_ url: URL, username: String, password: String) -> URLAuthenticationChallenge {
let scheme = url.scheme
let host = url.host ?? ""
let port = (url as NSURL).port?.intValue ?? 80
let credential = URLCredential(user: username, password: password, persistence: .none)
let protectionSpace = URLProtectionSpace(host: host,
port: port,
protocol: scheme,
realm: "Secure Site",
authenticationMethod: nil)
return URLAuthenticationChallenge(protectionSpace: protectionSpace,
proposedCredential: credential,
previousFailureCount: 0,
failureResponse: nil,
error: nil,
sender: MockChallengeSender())
}
fileprivate func hostnameFactory(_ row: SDRow) -> String {
return row["hostname"] as! String
}
fileprivate func rawQueryForAllLogins() -> Deferred<Maybe<Cursor<String>>> {
let projection = MainLoginColumns
let sql =
"SELECT \(projection) FROM " +
"\(TableLoginsLocal) WHERE is_deleted = 0 " +
"UNION ALL " +
"SELECT \(projection) FROM " +
"\(TableLoginsMirror) WHERE is_overridden = 0 " +
"ORDER BY hostname ASC"
return db.runQuery(sql, args: nil, factory: hostnameFactory)
}
func testChallengeMatchesLoginEntry() {
let login = Login.createWithHostname("https://securesite.com", username: "username", password: "password", formSubmitURL: "https://submit.me")
logins.addLogin(login).succeeded()
let challenge = mockChallengeForURL(URL(string: "https://securesite.com")!, username: "username", password: "password")
let result = Authenticator.findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: logins).value.successValue!
XCTAssertNotNil(result)
XCTAssertEqual(result?.user, "username")
XCTAssertEqual(result?.password, "password")
}
func testChallengeMatchesSingleMalformedLoginEntry() {
// Since Login has been updated to not store schemeless URL, write directly to simulate a malformed URL
let malformedLogin = MockMalformableLogin.createWithHostname("malformed.com", username: "username", password: "password", formSubmitURL: "https://submit.me")
logins.addLogin(malformedLogin).succeeded()
// Pre-condition: Check that the hostname is malformed
let oldHostname = rawQueryForAllLogins().value.successValue![0]
XCTAssertEqual(oldHostname, "malformed.com")
let challenge = mockChallengeForURL(URL(string: "https://malformed.com")!, username: "username", password: "password")
let result = Authenticator.findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: logins).value.successValue!
XCTAssertNotNil(result)
XCTAssertEqual(result?.user, "username")
XCTAssertEqual(result?.password, "password")
// Post-condition: Check that we updated the hostname to be not malformed
let newHostname = rawQueryForAllLogins().value.successValue![0]
XCTAssertEqual(newHostname, "https://malformed.com")
}
func testChallengeMatchesDuplicateLoginEntries() {
// Since Login has been updated to not store schemeless URL, write directly to simulate a malformed URL
let malformedLogin = MockMalformableLogin.createWithHostname("malformed.com", username: "malformed_username", password: "malformed_password", formSubmitURL: "https://submit.me")
logins.addLogin(malformedLogin).succeeded()
let login = Login.createWithHostname("https://malformed.com", username: "good_username", password: "good_password", formSubmitURL: "https://submit.me")
logins.addLogin(login).succeeded()
// Pre-condition: Verify that both logins were stored
let hostnames = rawQueryForAllLogins().value.successValue!
XCTAssertEqual(hostnames.count, 2)
XCTAssertEqual(hostnames[0], "https://malformed.com")
XCTAssertEqual(hostnames[1], "malformed.com")
let challenge = mockChallengeForURL(URL(string: "https://malformed.com")!, username: "username", password: "password")
let result = Authenticator.findMatchingCredentialsForChallenge(challenge, fromLoginsProvider: logins).value.successValue!
XCTAssertNotNil(result)
XCTAssertEqual(result?.user, "good_username")
XCTAssertEqual(result?.password, "good_password")
// Post-condition: Verify that malformed URL was removed
let newHostnames = rawQueryForAllLogins().value.successValue!
XCTAssertEqual(newHostnames.count, 1)
XCTAssertEqual(newHostnames[0], "https://malformed.com")
}
}

View file

@ -0,0 +1,94 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import UIKit
import XCTest
import Shared
import Storage
import WebKit
import Alamofire
@testable import Client
class ClientTests: XCTestCase {
func testSyncUA() {
let ua = UserAgent.syncUserAgent
let device = DeviceInfo.deviceModel()
let systemVersion = UIDevice.current.systemVersion
let expectedRegex = "^Firefox-iOS-Sync/[0-9\\.]+b[0-9]* \\(\(device); iPhone OS \(systemVersion)\\) \\([-_A-Za-z0-9= \\(\\)]+\\)$"
let loc = ua.range(of: expectedRegex, options: NSString.CompareOptions.regularExpression)
XCTAssertTrue(loc != nil, "Sync UA is as expected. Was \(ua)")
}
// Simple test to make sure the WKWebView UA matches the expected FxiOS pattern.
func testUserAgent() {
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
let compare: (String) -> Bool = { ua in
let range = ua.range(of: "^Mozilla/5\\.0 \\(.+\\) AppleWebKit/[0-9\\.]+ \\(KHTML, like Gecko\\) FxiOS/\(appVersion)b[0-9]* Mobile/[A-Za-z0-9]+ Safari/[0-9\\.]+$", options: NSString.CompareOptions.regularExpression)
return range != nil
}
XCTAssertTrue(compare(UserAgent.defaultUserAgent()), "User agent computes correctly.")
XCTAssertTrue(compare(UserAgent.cachedUserAgent(checkiOSVersion: true)!), "User agent is cached correctly.")
let expectation = self.expectation(description: "Found Firefox user agent")
let webView = WKWebView()
webView.evaluateJavaScript("navigator.userAgent") { result, error in
let userAgent = result as! String
if compare(userAgent) {
expectation.fulfill()
} else {
XCTFail("User agent did not match expected pattern! \(userAgent)")
}
}
waitForExpectations(timeout: 5, handler: nil)
}
func testDesktopUserAgent() {
let compare: (String) -> Bool = { ua in
let range = ua.range(of: "^Mozilla/5\\.0 \\(Macintosh; Intel Mac OS X [0-9_]+\\) AppleWebKit/[0-9\\.]+ \\(KHTML, like Gecko\\) Safari/[0-9\\.]+$", options: NSString.CompareOptions.regularExpression)
return range != nil
}
XCTAssertTrue(compare(UserAgent.desktopUserAgent()), "Desktop user agent computes correctly.")
}
/// Our local server should only accept whitelisted hosts (localhost and 127.0.0.1).
/// All other localhost equivalents should return 403.
func testDisallowLocalhostAliases() {
// Allowed local hosts. The first two are equivalent since iOS forwards an
// empty host to localhost.
[ "localhost",
"",
"127.0.0.1",
].forEach { XCTAssert(hostIsValid($0), "\($0) host should be valid.") }
// Disallowed local hosts. WKWebView will direct them to our server, but the server
// should reject them.
[ "[::1]",
"2130706433",
"0",
"127.00.00.01",
"017700000001",
"0x7f.0x0.0x0.0x1"
].forEach { XCTAssertFalse(hostIsValid($0), "\($0) host should not be valid.") }
}
fileprivate func hostIsValid(_ host: String) -> Bool {
let expectation = self.expectation(description: "Validate host for \(host)")
let request = URLRequest(url: URL(string: "http://\(host):6571/about/license")!)
var response: HTTPURLResponse?
Alamofire.request(request).authenticate(usingCredential: WebServer.sharedInstance.credentials).response { (res) -> Void in
response = res.response
expectation.fulfill()
}
waitForExpectations(timeout: 100, handler: nil)
return response?.statusCode == 200
}
}

View file

@ -0,0 +1,60 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import Shared
import Storage
import Sync
import UIKit
import XCTest
class CustomSearchEnginesTest: XCTestCase {
func testgetSearchTemplate() {
let profile = MockBrowserProfile(localName: "customSearchTests")
let customSearchEngineForm = CustomSearchViewController()
customSearchEngineForm.profile = profile
let template = customSearchEngineForm.getSearchTemplate(withString: "https://github.com/search=%s")
XCTAssertEqual(template, "https://github.com/search={searchTerms}")
let badTemplate = customSearchEngineForm.getSearchTemplate(withString: "https://github.com/search=blah")
XCTAssertNil(badTemplate)
}
func testaddSearchEngine() {
let profile = MockBrowserProfile(localName: "customSearchTests")
let customSearchEngineForm = CustomSearchViewController()
customSearchEngineForm.profile = profile
let q = "http://www.google.ca/?#q=%s"
let title = "YASE"
let expectation = self.expectation(description: "Waiting on favicon fetching")
customSearchEngineForm.createEngine(forQuery: q, andName: title).uponQueue(DispatchQueue.main) { result in
XCTAssertNotNil(result.successValue, "Make sure the new engine is not nil")
let engine = result.successValue!
XCTAssertEqual(engine.shortName, title)
XCTAssertNotNil(engine.image)
XCTAssertEqual(engine.searchTemplate, "http://www.google.ca/?#q={searchTerms}")
expectation.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
}
func testaddSearchEngineFailure() {
let profile = MockBrowserProfile(localName: "customSearchTests")
let customSearchEngineForm = CustomSearchViewController()
customSearchEngineForm.profile = profile
let q = "isthisvalid.com/hhh%s"
let title = "YASE"
let expectation = self.expectation(description: "Waiting on favicon fetching")
customSearchEngineForm.createEngine(forQuery: q, andName: title).uponQueue(DispatchQueue.main) { result in
XCTAssertNil(result.successValue, "Make sure the new engine is nil")
expectation.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
}
}

View file

@ -0,0 +1,76 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Storage
import XCTest
class FileAccessorTests: XCTestCase {
fileprivate var testDir: String!
fileprivate var files: FileAccessor!
override func setUp() {
let docPath: NSString = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] as NSString
files = FileAccessor(rootPath: docPath.appendingPathComponent("filetest"))
testDir = try! files.getAndEnsureDirectory()
try! files.removeFilesInDirectory()
}
func testFileAccessor() {
// Test existence.
XCTAssertFalse(files.exists("foo"), "File doesn't exist")
createFile("foo")
XCTAssertTrue(files.exists("foo"), "File exists")
// Test moving.
do {
try files.move("foo", toRelativePath: "bar")
XCTAssertFalse(files.exists("foo"), "Old doesn't exist")
XCTAssertTrue(files.exists("bar"), "New file exists")
} catch {
XCTFail("Unable to move 'foo' to 'bar' \(error)")
}
do {
try files.move("bar", toRelativePath: "foo/bar")
XCTAssertFalse(files.exists("bar"), "Old doesn't exist")
XCTAssertTrue(files.exists("foo/bar"), "New file exists")
} catch {
XCTFail("Unable to move 'bar' to 'foo/bar' \(error)")
}
// Test removal.
do {
XCTAssertTrue(files.exists("foo"), "File exists")
try files.remove("foo")
XCTAssertFalse(files.exists("foo"), "File removed")
} catch {
XCTFail("Unable to remove 'foo' \(error)")
}
// Test directory creation and path.
do {
XCTAssertFalse(files.exists("foo"), "Directory doesn't exist")
let path = try files.getAndEnsureDirectory("foo")
var isDirectory = ObjCBool(false)
FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
XCTAssertTrue(isDirectory.boolValue, "Directory exists")
} catch {
XCTFail("Unable to find directory 'foo' \(error)")
}
}
fileprivate func createFile(_ filename: String) {
let path = (testDir as NSString).appendingPathComponent(filename)
let success: Bool
do {
try "foo".write(toFile: path, atomically: false, encoding: String.Encoding.utf8)
success = true
} catch _ {
success = false
}
XCTAssertTrue(success, "Wrote to \(path)")
}
}

View file

@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import UIKit
import XCTest
@testable import Client
import Shared
class FxADeepLinkingTests: XCTestCase {
var profile: MockProfile!
var vc: FxAContentViewController!
var expectUrl = URL(string: "https://accounts.firefox.com/signin?service=sync&context=fx_ios_v1&signin=test&utm_source=somesource&entrypoint=one")
override func setUp() {
super.setUp()
self.profile = MockProfile()
self.vc = FxAContentViewController(profile: self.profile)
}
func testLaunchWithNilOptions() {
let testUrl = self.vc.createFxAURLWith(nil, profile: self.profile)
// Should use default urls for nil options
XCTAssertEqual(testUrl, self.vc.profile.accountConfiguration.signInURL)
}
func testLaunchWithOptions() {
let url = URL(string: "firefox://fxa-signin?signin=test&utm_source=somesource&entrypoint=one&ignore=this")
let query = url!.getQuery()
let fxaOptions = FxALaunchParams(query: query)
let testUrl = self.vc.createFxAURLWith(fxaOptions, profile: self.profile)
XCTAssertEqual(testUrl, expectUrl!)
}
func testDoesntOverrideServiceContext() {
let url = URL(string: "firefox://fxa-signin?service=asdf&context=123&signin=test&entrypoint=one&utm_source=somesource&ignore=this")
let query = url!.getQuery()
let fxaOptions = FxALaunchParams(query: query)
let testUrl = self.vc.createFxAURLWith(fxaOptions, profile: self.profile)
XCTAssertEqual(testUrl, expectUrl!)
}
}

View file

@ -0,0 +1,153 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Account
@testable import Client
import Foundation
import FxA
import SwiftyJSON
import XCTest
class FxAPushMessageTest: XCTestCase {
func testMessage_subscriptionDecryption() {
var userInfo: [AnyHashable: Any] = [
"chid": "034f52789f7b44ecaf119cc59231cdc1",
"enc": "keyid=p256dh;salt=mYBHM3B_oXlEjV0HfgHZ1A",
"body": "_tZf65gKC23STnTNuhtSSbrg1LScGiLjO4GOIuHlCGIFFEzcwsB-J-s3pe3qu2d24A-sKwVyolmShlMBEvEX_34f6FgXMs3k35g4u5STKgJMQxZ8VFDjtQqQfxfSIEt35pdKaPwXKH2zbs0xHC3qEJ0YMc60Eq8uAuQF7FQZ7ts",
"cryptokey": "keyid=p256dh;dh=BNLZ2IWMNGioofzMBnSijySib0Pa-lwgBfLYIhqvvmKxprKgEh6JCDB2DBUmj9BuJCk6xJvBbPd-4x_8tb-qIPM;p256ecdsa=BP-OR33RzQSlrzD7_d1kYE9i9WjSIQAKTuhHxYNiPEF0i-wxeNIIwxthwU7zBTbumyxFUeydrmxcVKXugjBImRU",
"con": "aesgcm",
"ver": "gAAAAABY5hUl_Pi1bVI4ptqKzPWq_aulqXEO1eOx_ncUuwoz8zItWWLA4Ix1ZbDLTOkurqK1vE3N3y1ZXsp_MZ69QJYRuWg5V_3T_XUNVBxj8dnDcNAE-ep9_M5xeKiNdngdww-cqqMOxf-tI5ZoR2nQFqWSs51XGwMMIdsGNmUPqnR4w2Rzt6FYz-AsGCrHXJ3bmYbwpq4P",
]
let subscription = PushSubscription(channelID: "channel",
endpoint: URL(string: "https://example.ru")!,
p256dhPrivateKey: "UDnicgor_Il7cLNTqSt--SrEblNBbgPA2yXAQ_b31xI",
p256dhPublicKey: "BB4XdAhpVVU45NYSXHpRiubMYoaeb0A-y5aSGE437YKGHQihlvlZMv5D0ebK6WmFqzpIr217Kv9oCbZVDp1KGK4",
authKey: "0gH7RiYYMhfHDQ1L1X4RMw")
let body = userInfo["body"] as! String
let enc = userInfo["enc"] as! String
let cryptokey = userInfo["cryptokey"] as! String
guard let plaintext = subscription.aesgcm(payload: body, encryptionHeader: enc, cryptoHeader: cryptokey) else {
return XCTFail("Decryption failed")
}
let json = JSON(parseJSON: plaintext)
XCTAssertEqual(json["command"].stringValue, "fxaccounts:device_connected")
}
func testMessageHandler() {
let subscription = PushSubscription(channelID: "channel",
endpoint: URL(string: "https://example.ru")!,
p256dhPrivateKey: "t7dcZIN4w37UYjE6u3lBLB0WOShxqelkbJFKKzMDSsE",
p256dhPublicKey: "BIXFDlhppL2lc5GcIXbGPa1iVdJn5ULYaF1ltJY9Qm17-tIC_9eEZXtalPpMRXsFmEKhdn2ttg3KQ3t-ztQ3ShQ",
authKey: "a8E3EO5F6FFWdv4hAGyGyw")
let userInfo: [AnyHashable: Any] = [
AnyHashable("chid"): "f6defa012a6249e58bbfbf2995f8d425",
AnyHashable("enc"): "keyid=p256dh;salt=nslkEsUqUg5sQ7_sRZfOjg",
AnyHashable("body"): "Q1t-ttSwQMxUI64Ls3vOU-hE_qg1AIUzyLQSpEkx-8JITh5UJ7oq25faEc8XPoTYQoaHQ2d--QIK_yVorbt-0Yr7IO4BmtSSX-e4kSx76fWzqKjEpEt7Vr3Av5seEBeoAT2FZRzehkFjNVWoTw",
AnyHashable("cryptokey"): "keyid=p256dh;dh=BNfUPK_8eUTZGOyXq07lthBfHeIxC2B7L_gF3cMGK1jVfDe9tlgxpHD_mbKrt3p12d7_O__wizhne2a1Eb7pZgk;p256ecdsa=BFSuld8S4PbRcgGe3OQPN9NyIOXx-ccUIMb0q6nIpH7Qf894wz0TIQTXQ7I7pWjZiN9KCdYVjNhyPtr1--37ois",
AnyHashable("con"): "aesgcm",
AnyHashable("ver"): "gAAAAABZLbG-m7EHhcMdrqs51SkESIZHsZjvw2QIu8LOeXxcKEy6wDVCprOKFAfJU44cinfJcDtCnO9EEyzpFt5e0HBDCLybGyThoZzmiod6zTLhTfAKZe-SyElSVCL0UDpJ_-U3UTUUHUaJXeRf0z6NvFM-uL39Jy-dwr3cuJoSDIcTPdChRPFiIS1hwokqMlxOn36azxOi",
]
let profile = MockProfile()
let account = FirefoxAccount(
configuration: FirefoxAccountConfigurationLabel.production.toConfiguration(),
email: "testtest@test.com",
uid: "uid",
deviceRegistration: nil,
stateKeyLabel: "xxx",
state: SeparatedState())
let registration = PushRegistration(uaid: "uaid", secret: "secret", subscription: subscription)
account.pushRegistration = registration
profile.setAccount(account)
let handler = FxAPushMessageHandler(with: profile)
let expectation = XCTestExpectation()
handler.handle(userInfo: userInfo).upon { maybe in
XCTAssertTrue(maybe.isSuccess)
XCTAssertEqual(maybe.successValue!, PushMessage.collectionChanged(collections: ["clients", "tabs"]))
expectation.fulfill()
}
wait(for: [expectation], timeout: 10)
}
func createHandler(_ profile: Profile = MockProfile()) -> FxAPushMessageHandler {
let account = FirefoxAccount(
configuration: FirefoxAccountConfigurationLabel.production.toConfiguration(),
email: "testtest@test.com",
uid: "uid",
deviceRegistration: nil,
stateKeyLabel: "xxx",
state: SeparatedState())
profile.setAccount(account)
return FxAPushMessageHandler(with: profile)
}
func test_deviceConnected() {
let handler = createHandler()
let expectation = XCTestExpectation()
handler.handle(plaintext: "{\"command\":\"fxaccounts:device_connected\",\"data\":{\"deviceName\": \"Use Nightly on Desktop\"}}").upon { maybe in
XCTAssertTrue(maybe.isSuccess)
guard let message = maybe.successValue else {
return expectation.fulfill()
}
XCTAssertEqual(message, PushMessage.deviceConnected("Use Nightly on Desktop"))
expectation.fulfill()
}
wait(for: [expectation], timeout: 10)
}
func test_deviceDisconnected() {
let profile = MockProfile()
let handler = createHandler(profile)
let prefs = profile.prefs
let expectation = XCTestExpectation()
handler.handle(plaintext: "{\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\": \"not_this_device\"}}").upon { maybe in
XCTAssertTrue(maybe.isSuccess)
guard let message = maybe.successValue else {
return expectation.fulfill()
}
XCTAssertEqual(message.messageType, .deviceDisconnected)
XCTAssertFalse(prefs.boolForKey(PendingAccountDisconnectedKey) ?? false)
expectation.fulfill()
}
wait(for: [expectation], timeout: 10)
}
func test_thisDeviceDisconnected() {
let profile = MockProfile()
let handler = createHandler(profile)
let deviceRegistration = FxADeviceRegistration(id: "this-device-id", version: 1, lastRegistered: 0)
profile.account?.deviceRegistration = deviceRegistration
let prefs = profile.prefs
let expectation = XCTestExpectation()
handler.handle(plaintext: "{\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\": \"\(deviceRegistration.id)\"}}").upon { maybe in
guard let message = maybe.successValue else {
return expectation.fulfill()
}
XCTAssertEqual(message, PushMessage.thisDeviceDisconnected)
XCTAssertTrue(prefs.boolForKey(PendingAccountDisconnectedKey) ?? false)
expectation.fulfill()
}
wait(for: [expectation], timeout: 10)
}
}

View file

@ -0,0 +1,16 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import XCTest
class HomePageTests: XCTestCase {
let prefs = NSUserDefaultsPrefs(prefix: "PrefsTests")
func testHomePageSettingForInternalURLs() {
let helper = HomePageHelper(prefs: prefs)
helper.currentURL = URL(string: "http://localhost:6571")
XCTAssertNil(prefs.stringForKey(HomePageConstants.HomePageURLPrefKey))
}
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>10.6</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View file

@ -0,0 +1,235 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import Foundation
import Account
import ReadingList
import Shared
import Storage
import Sync
import XCTest
import Deferred
open class MockSyncManager: SyncManager {
open var isSyncing = false
open var lastSyncFinishTime: Timestamp?
open var syncDisplayState: SyncDisplayState?
open func hasSyncedHistory() -> Deferred<Maybe<Bool>> {
return deferMaybe(true)
}
private func completedWithStats(collection: String) -> Deferred<Maybe<SyncStatus>> {
return deferMaybe(SyncStatus.completed(SyncEngineStatsSession(collection: collection)))
}
open func syncClients() -> SyncResult { return completedWithStats(collection: "mock_clients") }
open func syncClientsThenTabs() -> SyncResult { return completedWithStats(collection: "mock_clientsandtabs") }
open func syncHistory() -> SyncResult { return completedWithStats(collection: "mock_history") }
open func syncLogins() -> SyncResult { return completedWithStats(collection: "mock_logins") }
open func mirrorBookmarks() -> SyncResult { return completedWithStats(collection: "mock_bookmarks") }
open func syncEverything(why: SyncReason) -> Success {
return succeed()
}
open func syncNamedCollections(why: SyncReason, names: [String]) -> Success {
return succeed()
}
open func beginTimedSyncs() {}
open func endTimedSyncs() {}
open func applicationDidBecomeActive() {
self.beginTimedSyncs()
}
open func applicationDidEnterBackground() {
self.endTimedSyncs()
}
open func onNewProfile() {
}
open func onAddedAccount() -> Success {
return succeed()
}
open func onRemovedAccount(_ account: FirefoxAccount?) -> Success {
return succeed()
}
open func hasSyncedLogins() -> Deferred<Maybe<Bool>> {
return deferMaybe(true)
}
}
open class MockTabQueue: TabQueue {
open func addToQueue(_ tab: ShareItem) -> Success {
return succeed()
}
open func getQueuedTabs() -> Deferred<Maybe<Cursor<ShareItem>>> {
return deferMaybe(ArrayCursor<ShareItem>(data: []))
}
open func clearQueuedTabs() -> Success {
return succeed()
}
}
open class MockPanelDataObservers: PanelDataObservers {
override init(profile: Profile) {
super.init(profile: profile)
self.activityStream = MockActivityStreamDataObserver(profile: profile)
}
}
open class MockActivityStreamDataObserver: DataObserver {
public var profile: Profile
public weak var delegate: DataObserverDelegate?
init(profile: Profile) {
self.profile = profile
}
public func refreshIfNeeded(forceHighlights highlights: Bool, forceTopSites topsites: Bool) {
}
}
open class MockProfile: Profile {
// Read/Writeable properties for mocking
public var recommendations: HistoryRecommendations
public var places: BrowserHistory & Favicons & SyncableHistory & ResettableSyncStorage & HistoryRecommendations
public var files: FileAccessor
public var history: BrowserHistory & SyncableHistory & ResettableSyncStorage
public var logins: BrowserLogins & SyncableLogins & ResettableSyncStorage
public var syncManager: SyncManager!
public lazy var panelDataObservers: PanelDataObservers = {
return MockPanelDataObservers(profile: self)
}()
var db: BrowserDB
fileprivate let name: String = "mockaccount"
init() {
files = MockFiles()
syncManager = MockSyncManager()
logins = MockLogins(files: files)
db = BrowserDB(filename: "mock.db", schema: BrowserSchema(), files: files)
places = SQLiteHistory(db: self.db, prefs: MockProfilePrefs())
recommendations = places
history = places
}
public func localName() -> String {
return name
}
public func reopen() {
}
public func shutdown() {
}
public var isShutdown: Bool = false
public var favicons: Favicons {
return self.places
}
lazy public var queue: TabQueue = {
return MockTabQueue()
}()
lazy public var metadata: Metadata = {
return SQLiteMetadata(db: self.db)
}()
lazy public var isChinaEdition: Bool = {
return Locale.current.identifier == "zh_CN"
}()
lazy public var certStore: CertStore = {
return CertStore()
}()
lazy public var bookmarks: BookmarksModelFactorySource & KeywordSearchSource & SyncableBookmarks & LocalItemSource & MirrorItemSource & ShareToDestination = {
// Make sure the rest of our tables are initialized before we try to read them!
// This expression is for side-effects only.
let p = self.places
return MergedSQLiteBookmarks(db: self.db)
}()
lazy public var searchEngines: SearchEngines = {
return SearchEngines(prefs: self.prefs, files: self.files)
}()
lazy public var prefs: Prefs = {
return MockProfilePrefs()
}()
lazy public var readingList: ReadingListService? = {
return ReadingListService(profileStoragePath: self.files.rootPath as String)
}()
lazy public var recentlyClosedTabs: ClosedTabsStore = {
return ClosedTabsStore(prefs: self.prefs)
}()
internal lazy var remoteClientsAndTabs: RemoteClientsAndTabs = {
return SQLiteRemoteClientsAndTabs(db: self.db)
}()
fileprivate lazy var syncCommands: SyncCommands = {
return SQLiteRemoteClientsAndTabs(db: self.db)
}()
public let accountConfiguration: FirefoxAccountConfiguration = ProductionFirefoxAccountConfiguration()
var account: FirefoxAccount?
public func hasAccount() -> Bool {
return account != nil
}
public func hasSyncableAccount() -> Bool {
return account?.actionNeeded == FxAActionNeeded.none
}
public func getAccount() -> FirefoxAccount? {
return account
}
public func setAccount(_ account: FirefoxAccount) {
self.account = account
self.syncManager.onAddedAccount()
}
public func flushAccount() {}
public func removeAccount() {
let old = self.account
self.account = nil
self.syncManager.onRemovedAccount(old)
}
public func getClients() -> Deferred<Maybe<[RemoteClient]>> {
return deferMaybe([])
}
public func getClientsAndTabs() -> Deferred<Maybe<[ClientAndTabs]>> {
return deferMaybe([])
}
public func getCachedClientsAndTabs() -> Deferred<Maybe<[ClientAndTabs]>> {
return deferMaybe([])
}
public func storeTabs(_ tabs: [RemoteTab]) -> Deferred<Maybe<Int>> {
return deferMaybe(0)
}
public func sendItems(_ items: [ShareItem], toClients clients: [RemoteClient]) -> Deferred<Maybe<SyncStatus>> {
return deferMaybe(SyncStatus.notStarted(.offline))
}
}

View file

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Storage
import Deferred
import Shared
/*
* A class that adheres to all the requirements for a profile's history property
* with all of the methods set to fatalError. Use this class if you're looking to
* mock out parts of the history API
*/
class MockableHistory: BrowserHistory, SyncableHistory, ResettableSyncStorage {
func getFrecentHistory() -> FrecentHistory { fatalError() }
func getTopSitesWithLimit(_ limit: Int) -> Deferred<Maybe<Cursor<Site>>> { fatalError() }
func addLocalVisit(_ visit: SiteVisit) -> Success { fatalError() }
func clearHistory() -> Success { fatalError() }
func removeHistoryForURL(_ url: String) -> Success { fatalError() }
func removeSiteFromTopSites(_ site: Site) -> Success { fatalError() }
func removeHostFromTopSites(_ host: String) -> Success { fatalError() }
func clearTopSitesCache() -> Success { fatalError() }
func removeFromPinnedTopSites(_ site: Site) -> Success { fatalError() }
func addPinnedTopSite(_ site: Site) -> Success { fatalError() }
func getPinnedTopSites() -> Deferred<Maybe<Cursor<Site>>> { fatalError() }
func getSitesByLastVisit(_ limit: Int) -> Deferred<Maybe<Cursor<Site>>> { fatalError() }
func setTopSitesNeedsInvalidation() { fatalError() }
func updateTopSitesCacheIfInvalidated() -> Deferred<Maybe<Bool>> { fatalError() }
func setTopSitesCacheSize(_ size: Int32) { fatalError() }
func onRemovedAccount() -> Success { fatalError() }
func ensurePlaceWithURL(_ url: String, hasGUID guid: GUID) -> Success { fatalError() }
func deleteByGUID(_ guid: GUID, deletedAt: Timestamp) -> Success { fatalError() }
func storeRemoteVisits(_ visits: [Visit], forGUID guid: GUID) -> Success { fatalError() }
func insertOrUpdatePlace(_ place: Place, modified: Timestamp) -> Deferred<Maybe<GUID>> { fatalError() }
func getModifiedHistoryToUpload() -> Deferred<Maybe<[(Place, [Visit])]>> { fatalError() }
func getDeletedHistoryToUpload() -> Deferred<Maybe<[GUID]>> { fatalError() }
func markAsSynchronized(_: [GUID], modified: Timestamp) -> Deferred<Maybe<Timestamp>> { fatalError() }
func markAsDeleted(_ guids: [GUID]) -> Success { fatalError() }
func doneApplyingRecordsAfterDownload() -> Success { fatalError() }
func doneUpdatingMetadataAfterUpload() -> Success { fatalError() }
func hasSyncedHistory() -> Deferred<Maybe<Bool>> { fatalError() }
func resetClient() -> Success { fatalError() }
}

View file

@ -0,0 +1,84 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Shared
import XCTest
@testable import Client
private class MockDataObserverDelegate: DataObserverDelegate {
var didInvalidateCount = 0
var willInvalidateCount = 0
var highlightsRefreshCount = 0
var topSitesRefreshCount = 0
func didInvalidateDataSources(refresh forced: Bool, highlightsRefreshed: Bool, topSitesRefreshed: Bool) {
didInvalidateCount += 1
if highlightsRefreshed {
highlightsRefreshCount += 1
}
if topSitesRefreshed {
topSitesRefreshCount += 1
}
}
func willInvalidateDataSources(forceHighlights highlights: Bool, forceTopSites topSites: Bool) {
willInvalidateCount += 1
}
}
class PanelDataObserversTests: XCTestCase {
func testActivityStreamDelegates() {
let profile = MockProfile()
let observer = ActivityStreamDataObserver(profile: profile)
let delegate = MockDataObserverDelegate()
observer.delegate = delegate
NotificationCenter.default.post(name: NotificationFirefoxAccountChanged,
object: nil)
NotificationCenter.default.post(name: NotificationProfileDidFinishSyncing,
object: nil)
NotificationCenter.default.post(name: NotificationPrivateDataClearedHistory,
object: nil)
waitForCondition(timeout: 5) { delegate.didInvalidateCount == 3 && delegate.willInvalidateCount == 3 }
}
func testHighlightsCacheInvalidation20Min() {
let profile = MockProfile()
let observer = ActivityStreamDataObserver(profile: profile)
let delegate = MockDataObserverDelegate()
observer.delegate = delegate
// Set to 20min since refresh
profile.prefs.setLong(Date.now() - (OneMinuteInMilliseconds * 20), forKey: PrefsKeys.ASLastInvalidation)
observer.refreshIfNeeded(forceHighlights: false, forceTopSites: false)
waitForCondition(timeout: 5) { delegate.highlightsRefreshCount == 1 }
}
func testHighlightEmptyCache() {
let profile = MockProfile()
let observer = ActivityStreamDataObserver(profile: profile)
let delegate = MockDataObserverDelegate()
observer.delegate = delegate
// Set to no validation key
profile.prefs.removeObjectForKey(PrefsKeys.ASLastInvalidation)
observer.refreshIfNeeded(forceHighlights: false, forceTopSites: false)
waitForCondition(timeout: 5) { delegate.highlightsRefreshCount == 1 }
}
func testHighlightActiveCache() {
let profile = MockProfile()
let observer = ActivityStreamDataObserver(profile: profile)
let delegate = MockDataObserverDelegate()
observer.delegate = delegate
// Set to 10min since refresh
profile.prefs.setLong(Date.now() - (OneMinuteInMilliseconds * 10), forKey: PrefsKeys.ASLastInvalidation)
observer.refreshIfNeeded(forceHighlights: false, forceTopSites: false)
waitForCondition(timeout: 5) { delegate.didInvalidateCount == 1 && delegate.highlightsRefreshCount == 0 }
}
}

View file

@ -0,0 +1,104 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import XCTest
import JSONSchema
import Alamofire
import Deferred
import Shared
@testable import SyncTelemetry
private let mockTopic = PingCentreTopic(name: "ios-mock", schema: Schema([
"type": "object",
"properties": [
"title": ["type": "string"]
],
"required": [
"title"
]
]))
private var receivedNetworkRequests = [URLRequest]()
// Used to mock the network so we don't need to rely on the interweb for our unit tests.
class MockingURLProtocol: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
return request.url?.scheme == "https" && request.httpMethod == "POST"
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
receivedNetworkRequests.append(request)
let response = HTTPURLResponse(url: request.url!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: [:])
self.client?.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed)
self.client?.urlProtocol(self, didLoad: "".data(using: String.Encoding.utf8)!)
self.client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {
//no-op
}
}
class PingCentreTests: XCTestCase {
var manager: SessionManager!
var client: PingCentreClient!
override func setUp() {
super.setUp()
let configuration = URLSessionConfiguration.default
configuration.protocolClasses!.insert(MockingURLProtocol.self, at: 0)
self.manager = SessionManager(configuration: configuration)
self.client = DefaultPingCentreImpl(topic: mockTopic, endpoint: .staging, clientID: "fakeID", manager: self.manager)
}
override func tearDown() {
receivedNetworkRequests = []
}
func testSendPing() {
let validPing = [
"title": "Test!"
]
let invalidPing = [String: AnyObject]()
client.sendPing(validPing, validate: true).succeeded()
let validationError = client.sendPing(invalidPing, validate: true).value
XCTAssertNotNil(validationError.failureValue)
XCTAssertTrue(validationError.failureValue! is PingValidationError)
// Double check that we actually sent the successful ping and not the invalid one
XCTAssertTrue(receivedNetworkRequests.count == 1)
}
func testSendBatch() {
let validPingA = ["title": "A"]
let validPingB = ["title": "B"]
let invalidPingC = [String: AnyObject]()
client.sendBatch([validPingA, validPingB], validate: true).succeeded()
let validationError = client.sendBatch([validPingA, invalidPingC], validate: true).value
XCTAssertNotNil(validationError.failureValue)
XCTAssertTrue(validationError.failureValue! is PingValidationError)
// Double check that we actually sent the successful ping and not the invalid one
XCTAssertTrue(receivedNetworkRequests.count == 1)
client.sendBatch([], validate: true).succeeded()
// Double check that we didn't send the empty payloads request
XCTAssertTrue(receivedNetworkRequests.count == 1)
}
}

View file

@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import UIKit
import GCDWebServers
import XCTest
@testable import Client
class PocketStoriesTests: XCTestCase {
var pocketAPI: String!
let webServer: GCDWebServer = GCDWebServer()
/// Setup a basic web server that binds to a random port and that has one default handler on /hello
fileprivate func setupWebServer() {
let path = Bundle(for: type(of: self)).path(forResource: "pocketglobalfeed", ofType: "json")
let data = try! Data(contentsOf: URL(fileURLWithPath: path!))
webServer.addHandler(forMethod: "GET", path: "/pocketglobalfeed", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in
return GCDWebServerDataResponse(data: data, contentType: "application/json")
}
if webServer.start(withPort: 0, bonjourName: nil) == false {
XCTFail("Can't start the GCDWebServer")
}
pocketAPI = "http://localhost:\(webServer.port)/pocketglobalfeed"
}
override func setUp() {
super.setUp()
setupWebServer()
}
override func tearDown() {
super.tearDown()
}
func testPocketStoriesCaching() {
let expect = expectation(description: "Pocket")
let PocketFeed = Pocket(endPoint: pocketAPI)
PocketFeed.globalFeed(items: 4).upon { result in
let items = result
XCTAssertEqual(items.count, 2, "We are fetching a static feed. There are only 2 items in it")
self.webServer.stop() // Stop the webserver so we can check caching
// Try again now that the webserver is down
PocketFeed.globalFeed(items: 4).upon { result in
let items = result
XCTAssertEqual(items.count, 2, "We are fetching a static feed. There are only 2 items in it")
let item = items.first
//These are all not optional so they should never be nil.
//But lets check in case someone decides to change something
XCTAssertNotNil(item?.domain, "Why")
XCTAssertNotNil(item?.imageURL, "You")
XCTAssertNotNil(item?.storyDescription, "Do")
XCTAssertNotNil(item?.title, "This")
XCTAssertNotNil(item?.url, "?")
expect.fulfill()
}
}
waitForExpectations(timeout: 10, handler: nil)
}
}

View file

@ -0,0 +1,100 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import Foundation
import Shared
import XCTest
class PrefsTests: XCTestCase {
var prefs: NSUserDefaultsPrefs!
override func setUp() {
super.setUp()
prefs = NSUserDefaultsPrefs(prefix: "PrefsTests")
}
override func tearDown() {
prefs.clearAll()
super.tearDown()
}
func testClearPrefs() {
prefs.setObject("foo", forKey: "bar")
XCTAssertEqual(prefs.stringForKey("bar")!, "foo")
// Ensure clearing prefs is branch-specific.
let otherPrefs = NSUserDefaultsPrefs(prefix: "othermockaccount")
otherPrefs.clearAll()
XCTAssertEqual(prefs.stringForKey("bar")!, "foo")
prefs.clearAll()
XCTAssertNil(prefs.stringForKey("bar"))
}
func testStringForKey() {
XCTAssertNil(prefs.stringForKey("key"))
prefs.setObject("value", forKey: "key")
XCTAssertEqual(prefs.stringForKey("key")!, "value")
// Non-String values return nil.
prefs.setObject(1, forKey: "key")
XCTAssertNil(prefs.stringForKey("key"))
}
func testBoolForKey() {
XCTAssertNil(prefs.boolForKey("key"))
prefs.setObject(true, forKey: "key")
XCTAssertEqual(prefs.boolForKey("key")!, true)
prefs.setObject(false, forKey: "key")
XCTAssertEqual(prefs.boolForKey("key")!, false)
// We would like non-Bool values to return nil, but I can't figure out how to differentiate.
// Instead, this documents the undesired behaviour.
prefs.setObject(1, forKey: "key")
XCTAssertEqual(prefs.boolForKey("key")!, true)
prefs.setObject("1", forKey: "key")
XCTAssertNil(prefs.boolForKey("key"))
prefs.setObject("x", forKey: "key")
XCTAssertNil(prefs.boolForKey("key"))
}
func testStringArrayForKey() {
XCTAssertNil(prefs.stringArrayForKey("key"))
prefs.setObject(["value1", "value2"], forKey: "key")
XCTAssertEqual(prefs.stringArrayForKey("key")!, ["value1", "value2"])
// Non-[String] values return nil.
prefs.setObject(1, forKey: "key")
XCTAssertNil(prefs.stringArrayForKey("key"))
// [Non-String] values return nil.
prefs.setObject([1, 2], forKey: "key")
XCTAssertNil(prefs.stringArrayForKey("key"))
}
func testMockProfilePrefsRoundtripsTimestamps() {
let prefs = MockProfilePrefs().branch("baz")
let val: Timestamp = Date.now()
prefs.setLong(val, forKey: "foobar")
XCTAssertEqual(val, prefs.unsignedLongForKey("foobar")!)
}
func testMockProfilePrefsKeys() {
let prefs = MockProfilePrefs().branch("baz") as! MockProfilePrefs
let val: Timestamp = Date.now()
prefs.setLong(val, forKey: "foobar")
XCTAssertEqual(val, (prefs.things["baz.foobar"] as! NSNumber).uint64Value)
}
func testMockProfilePrefsClearAll() {
let prefs1 = MockProfilePrefs().branch("bar") as! MockProfilePrefs
let prefs2 = MockProfilePrefs().branch("baz") as! MockProfilePrefs
// Ensure clearing prefs is branch-specific.
prefs1.setInt(123, forKey: "foo")
prefs2.clearAll()
XCTAssertEqual(123, prefs1.intForKey("foo")!)
prefs1.clearAll()
XCTAssertNil(prefs1.intForKey("foo") as AnyObject?)
}
}

View file

@ -0,0 +1,28 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import Foundation
import Shared
import Storage
import SwiftKeychainWrapper
import XCTest
/*
* A base test type for tests that need a profile.
*/
class ProfileTest: XCTestCase {
func withTestProfile(_ callback: (_ profile: Profile) -> Void) {
callback(MockProfile())
}
func testNewProfileClearsExistingAuthenticationInfo() {
let authInfo = AuthenticationKeychainInfo(passcode: "1234")
KeychainWrapper.sharedAppContainerKeychain.setAuthenticationInfo(authInfo)
XCTAssertNotNil(KeychainWrapper.sharedAppContainerKeychain.authenticationInfo())
let _ = BrowserProfile(localName: "my_profile", clear: true)
XCTAssertNil(KeychainWrapper.sharedAppContainerKeychain.authenticationInfo())
}
}

View file

@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import XCTest
class RelativeDatesTests: XCTestCase {
func testRelativeDates() {
let dateOrig = Date()
var date = Date(timeInterval: 0, since: dateOrig)
XCTAssertEqual(date.toRelativeTimeString(), "just now")
date = Date(timeInterval: -10, since: dateOrig)
XCTAssertEqual(date.toRelativeTimeString(), "just now")
date = Date(timeInterval: -60, since: dateOrig)
XCTAssertEqual(date.toRelativeTimeString(), ("today at " + DateFormatter.localizedString(from: date, dateStyle: DateFormatter.Style.none, timeStyle: DateFormatter.Style.short)))
let calendar = Calendar.autoupdatingCurrent
date = calendar.date(byAdding: .day, value: -1, to: dateOrig)!
XCTAssertEqual(date.toRelativeTimeString(), "yesterday")
date = calendar.date(byAdding: .day, value: -2, to: dateOrig)!
XCTAssertEqual(date.toRelativeTimeString(), "this week")
date = calendar.date(byAdding: .day, value: -7, to: dateOrig)!
XCTAssertEqual(date.toRelativeTimeString(), "more than a week ago")
date = calendar.date(byAdding: .day, value: -7 * 5, to: dateOrig)!
XCTAssertEqual(date.toRelativeTimeString(), "more than a month ago")
date = Date(timeInterval: -60 * 60 * 24 * 7 * 5 * 2, since: dateOrig)
XCTAssertEqual(date.toRelativeTimeString(), DateFormatter.localizedString(from: date, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))
date = Date(timeInterval: -60 * 60 * 24 * 7 * 5 * 12 * 2, since: dateOrig)
XCTAssertEqual(date.toRelativeTimeString(), DateFormatter.localizedString(from: date, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short))
}
}

View file

@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import Shared
@testable import Storage
import Sync
import UIKit
import XCTest
class MockBrowserProfile: BrowserProfile {
var peekSyncManager: BrowserSyncManager {
return self.syncManager as! BrowserSyncManager
}
var peekTabs: SQLiteRemoteClientsAndTabs {
return self.remoteClientsAndTabs as! SQLiteRemoteClientsAndTabs
}
}
class MockEngineStateChanges: EngineStateChanges {
var collections: [String] = []
var enabled: [String] = []
var disabled: [String] = []
var clearWasCalled: Bool = false
func collectionsThatNeedLocalReset() -> [String] {
return self.collections
}
func enginesEnabled() -> [String] {
return self.enabled
}
func enginesDisabled() -> [String] {
return self.disabled
}
func clearLocalCommands() {
clearWasCalled = true
}
}
func assertClientsHaveGUIDsFromStorage(_ storage: RemoteClientsAndTabs, expected: [GUID]) {
let recs = storage.getClients().value.successValue
XCTAssertNotNil(recs)
XCTAssertEqual(expected, recs!.map { $0.guid! })
}
class ResetTests: XCTestCase {
func testResetting() {
let profile = MockBrowserProfile(localName: "testResetTests")
// Add a client.
let tabs = profile.peekTabs
XCTAssertTrue(tabs.insertOrUpdateClient(RemoteClient(guid: "abcdefghijkl", name: "Remote", modified: Date.now(), type: "mobile", formfactor: "tablet", os: "Windows", version: "55.0.1a", fxaDeviceId: "fxa1")).value.isSuccess)
_ = tabs.replaceRemoteDevices([RemoteDevice(id: "fxa1", name: "Device 1", type: "desktop", isCurrentDevice: false, lastAccessTime: 123)]).succeeded()
// Verify that it's there.
assertClientsHaveGUIDsFromStorage(tabs, expected: ["abcdefghijkl"])
// Tell the sync manager that "clients" has changed syncID.
let e = MockEngineStateChanges()
e.collections.append("clients")
XCTAssertTrue(profile.peekSyncManager.takeActionsOnEngineStateChanges(e).value.isSuccess)
// We threw away the command.
XCTAssertTrue(e.clearWasCalled)
// And now we have no local clients.
let empty = tabs.getClients().value.successValue
XCTAssertNotNil(empty)
XCTAssertEqual(empty!, [])
}
}

View file

@ -0,0 +1,174 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import Foundation
import XCTest
import Shared
private let DefaultSearchEngineName = "Google"
private let ExpectedEngineNames = ["Amazon.com", "Bing", "DuckDuckGo", "Google", "Twitter", "Wikipedia", "Yahoo"]
class SearchEnginesTests: XCTestCase {
func testIncludesExpectedEngines() {
// Verify that the set of shipped engines includes the expected subset.
let profile = MockProfile()
let engines = SearchEngines(prefs: profile.prefs, files: profile.files).orderedEngines
XCTAssertTrue((engines?.count)! >= ExpectedEngineNames.count)
for engineName in ExpectedEngineNames {
XCTAssertTrue(((engines?.filter { engine in engine.shortName == engineName })?.count)! > 0)
}
}
func testDefaultEngineOnStartup() {
// If this is our first run, Yahoo should be first for the en locale.
let profile = MockProfile()
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
XCTAssertEqual(engines.defaultEngine.shortName, DefaultSearchEngineName)
XCTAssertEqual(engines.orderedEngines[0].shortName, DefaultSearchEngineName)
}
func testAddingAndDeletingCustomEngines() {
let testEngine = OpenSearchEngine(engineID: "ATester", shortName: "ATester", image: UIImage(), searchTemplate: "http://firefox.com/find?q={searchTerm}", suggestTemplate: nil, isCustomEngine: true)
let profile = MockProfile()
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
engines.addSearchEngine(testEngine)
XCTAssertEqual(engines.orderedEngines[1].engineID, testEngine.engineID)
engines.deleteCustomEngine(testEngine)
let deleted = engines.orderedEngines.filter {$0 == testEngine}
XCTAssertEqual(deleted, [])
}
func testDefaultEngine() {
let profile = MockProfile()
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
let engineSet = engines.orderedEngines
engines.defaultEngine = (engineSet?[0])!
XCTAssertTrue(engines.isEngineDefault((engineSet?[0])!))
XCTAssertFalse(engines.isEngineDefault((engineSet?[1])!))
// The first ordered engine is the default.
XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[0].shortName)
engines.defaultEngine = (engineSet?[1])!
XCTAssertFalse(engines.isEngineDefault((engineSet?[0])!))
XCTAssertTrue(engines.isEngineDefault((engineSet?[1])!))
// The first ordered engine is the default.
XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[1].shortName)
let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files)
// The default engine should have been persisted.
XCTAssertTrue(engines2.isEngineDefault((engineSet?[1])!))
// The first ordered engine is the default.
XCTAssertEqual(engines.orderedEngines[0].shortName, engineSet?[1].shortName)
}
func testOrderedEngines() {
let profile = MockProfile()
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
engines.orderedEngines = [ExpectedEngineNames[4], ExpectedEngineNames[2], ExpectedEngineNames[0]].map { name in
for engine in engines.orderedEngines {
if engine.shortName == name {
return engine
}
}
XCTFail("Could not find engine: \(name)")
return engines.orderedEngines.first!
}
XCTAssertEqual(engines.orderedEngines[0].shortName, ExpectedEngineNames[4])
XCTAssertEqual(engines.orderedEngines[1].shortName, ExpectedEngineNames[2])
XCTAssertEqual(engines.orderedEngines[2].shortName, ExpectedEngineNames[0])
let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files)
// The ordering should have been persisted.
XCTAssertEqual(engines2.orderedEngines[0].shortName, ExpectedEngineNames[4])
XCTAssertEqual(engines2.orderedEngines[1].shortName, ExpectedEngineNames[2])
XCTAssertEqual(engines2.orderedEngines[2].shortName, ExpectedEngineNames[0])
// Remaining engines should be appended in alphabetical order.
XCTAssertEqual(engines2.orderedEngines[3].shortName, ExpectedEngineNames[1])
XCTAssertEqual(engines2.orderedEngines[4].shortName, ExpectedEngineNames[3])
XCTAssertEqual(engines2.orderedEngines[5].shortName, ExpectedEngineNames[5])
XCTAssertEqual(engines2.orderedEngines[6].shortName, ExpectedEngineNames[6])
}
func testQuickSearchEngines() {
let profile = MockProfile()
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
let engineSet = engines.orderedEngines
// You can't disable the default engine.
engines.defaultEngine = (engineSet?[1])!
engines.disableEngine((engineSet?[1])!)
XCTAssertTrue(engines.isEngineEnabled((engineSet?[1])!))
// The default engine is not included in the quick search engines.
XCTAssertEqual(0, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[1].shortName }.count)
// Enable and disable work.
engines.enableEngine((engineSet?[0])!)
XCTAssertTrue(engines.isEngineEnabled((engineSet?[0])!))
XCTAssertEqual(1, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[0].shortName }.count)
engines.disableEngine((engineSet?[0])!)
XCTAssertFalse(engines.isEngineEnabled((engineSet?[0])!))
XCTAssertEqual(0, engines.quickSearchEngines.filter { engine in engine.shortName == engineSet?[0].shortName }.count)
// Setting the default engine enables it.
engines.defaultEngine = (engineSet?[0])!
XCTAssertTrue(engines.isEngineEnabled((engineSet?[1])!))
// Setting the order may change the default engine, which enables it.
engines.orderedEngines = [(engineSet?[2])!, (engineSet?[1])!, (engineSet?[0])!]
XCTAssertTrue(engines.isEngineDefault((engineSet?[2])!))
XCTAssertTrue(engines.isEngineEnabled((engineSet?[2])!))
// The enabling should be persisted.
engines.enableEngine((engineSet?[2])!)
engines.disableEngine((engineSet?[1])!)
engines.enableEngine((engineSet?[0])!)
let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files)
XCTAssertTrue(engines2.isEngineEnabled((engineSet?[2])!))
XCTAssertFalse(engines2.isEngineEnabled((engineSet?[1])!))
XCTAssertTrue(engines2.isEngineEnabled((engineSet?[0])!))
}
func testSearchSuggestionSettings() {
let profile = MockProfile()
let engines = SearchEngines(prefs: profile.prefs, files: profile.files)
// By default, you should see search suggestions
XCTAssertTrue(engines.shouldShowSearchSuggestions)
// Setting should be persisted.
engines.shouldShowSearchSuggestions = false
let engines2 = SearchEngines(prefs: profile.prefs, files: profile.files)
XCTAssertFalse(engines2.shouldShowSearchSuggestions)
}
func testDirectoriesForLanguageIdentifier() {
XCTAssertEqual(
SearchEngines.directoriesForLanguageIdentifier("nl", basePath: "/tmp", fallbackIdentifier: "en"),
["/tmp/nl", "/tmp/en"]
)
XCTAssertEqual(
SearchEngines.directoriesForLanguageIdentifier("en-US", basePath: "/tmp", fallbackIdentifier: "en"),
["/tmp/en-US", "/tmp/en"]
)
XCTAssertEqual(
SearchEngines.directoriesForLanguageIdentifier("es-MX", basePath: "/tmp", fallbackIdentifier: "en"),
["/tmp/es-MX", "/tmp/es", "/tmp/en"]
)
XCTAssertEqual(
SearchEngines.directoriesForLanguageIdentifier("zh-Hans-CN", basePath: "/tmp", fallbackIdentifier: "en"),
["/tmp/zh-Hans-CN", "/tmp/zh-CN", "/tmp/zh", "/tmp/en"]
)
}
}

View file

@ -0,0 +1,155 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import GCDWebServers
@testable import Client
import UIKit
import XCTest
class SearchTests: XCTestCase {
func testParsing() {
let parser = OpenSearchParser(pluginMode: true)
let file = Bundle.main.path(forResource: "google", ofType: "xml", inDirectory: "SearchPlugins/en")
let engine: OpenSearchEngine! = parser.parse(file!, engineID: "google")
XCTAssertEqual(engine.shortName, "Google")
// Test regular search queries.
XCTAssertEqual(engine.searchURLForQuery("foobar")!.absoluteString, "https://www.google.com/search?q=foobar&ie=utf-8&oe=utf-8&client=firefox-b")
// Test search suggestion queries.
XCTAssertEqual(engine.suggestURLForQuery("foobar")!.absoluteString, "https://www.google.com/complete/search?client=firefox&q=foobar")
}
func testURIFixup() {
// Check valid URLs. We can load these after some fixup.
checkValidURL("http://www.mozilla.org", afterFixup: "http://www.mozilla.org")
checkValidURL("about:", afterFixup: "about:")
checkValidURL("about:config", afterFixup: "about:config")
checkValidURL("about: config", afterFixup: "about:%20config")
checkValidURL("file:///f/o/o", afterFixup: "file:///f/o/o")
checkValidURL("ftp://ftp.mozilla.org", afterFixup: "ftp://ftp.mozilla.org")
checkValidURL("foo.bar", afterFixup: "http://foo.bar")
checkValidURL(" foo.bar ", afterFixup: "http://foo.bar")
checkValidURL("1.2.3", afterFixup: "http://1.2.3")
// Check invalid URLs. These are passed along to the default search engine.
checkInvalidURL("foobar")
checkInvalidURL("foo bar")
checkInvalidURL("mozilla. org")
checkInvalidURL("123")
checkInvalidURL("a/b")
checkInvalidURL("创业咖啡")
checkInvalidURL("创业咖啡 中国")
checkInvalidURL("创业咖啡. 中国")
}
func testURIFixupPunyCode() {
checkValidURL("http://创业咖啡.中国/", afterFixup: "http://xn--vhq70hq9bhxa.xn--fiqs8s/")
checkValidURL("创业咖啡.中国", afterFixup: "http://xn--vhq70hq9bhxa.xn--fiqs8s")
checkValidURL(" 创业咖啡.中国 ", afterFixup: "http://xn--vhq70hq9bhxa.xn--fiqs8s")
}
fileprivate func checkValidURL(_ beforeFixup: String, afterFixup: String) {
XCTAssertEqual(URIFixup.getURL(beforeFixup)!.absoluteString, afterFixup)
}
fileprivate func checkInvalidURL(_ beforeFixup: String) {
XCTAssertNil(URIFixup.getURL(beforeFixup))
}
func testSuggestClient() {
let webServerBase = startMockSuggestServer()
let engine = OpenSearchEngine(engineID: "mock", shortName: "Mock engine", image: UIImage(), searchTemplate: "", suggestTemplate: "\(webServerBase)?q={searchTerms}",
isCustomEngine: false)
let client = SearchSuggestClient(searchEngine: engine, userAgent: "Fx-testSuggestClient")
let query1 = self.expectation(description: "foo query")
client.query("foo", callback: { response, error in
withExtendedLifetime(client) {
if error != nil {
XCTFail("Error: \(error?.description ?? "nil")")
}
XCTAssertEqual(response![0], "foo")
XCTAssertEqual(response![1], "foo2")
XCTAssertEqual(response![2], "foo you")
query1.fulfill()
}
})
waitForExpectations(timeout: 10, handler: nil)
let query2 = self.expectation(description: "foo bar query")
client.query("foo bar", callback: { response, error in
withExtendedLifetime(client) {
if error != nil {
XCTFail("Error: \(error?.description ?? "nil")")
}
XCTAssertEqual(response![0], "foo bar soap")
XCTAssertEqual(response![1], "foo barstool")
XCTAssertEqual(response![2], "foo bartender")
query2.fulfill()
}
})
waitForExpectations(timeout: 10, handler: nil)
}
func testExtractingOfSearchTermsFromURL() {
let parser = OpenSearchParser(pluginMode: true)
var file = Bundle.main.path(forResource: "google", ofType: "xml", inDirectory: "SearchPlugins/en")
let googleEngine: OpenSearchEngine! = parser.parse(file!, engineID: "google")
// create URL
let searchTerm = "Foo Bar"
let encodedSeachTerm = searchTerm.replacingOccurrences(of: " ", with: "+")
let googleSearchURL = URL(string: "https://www.google.com/search?q=\(encodedSeachTerm)&ie=utf-8&oe=utf-8&gws_rd=cr&ei=I0UyVp_qK4HtUoytjagM")
let duckDuckGoSearchURL = URL(string: "https://duckduckgo.com/?q=\(encodedSeachTerm)&ia=about")
let invalidSearchURL = URL(string: "https://www.google.co.uk")
// check it correctly matches google search term given google config
XCTAssertEqual(searchTerm, googleEngine.queryForSearchURL(googleSearchURL))
// check it doesn't match when the URL is not a search URL
XCTAssertNil(googleEngine.queryForSearchURL(invalidSearchURL))
// check that it matches given a different configuration
file = Bundle.main.path(forResource: "duckduckgo", ofType: "xml", inDirectory: "SearchPlugins/en")
let duckDuckGoEngine: OpenSearchEngine! = parser.parse(file!, engineID: "duckduckgo")
XCTAssertEqual(searchTerm, duckDuckGoEngine.queryForSearchURL(duckDuckGoSearchURL))
// check it doesn't match search URLs for different configurations
XCTAssertNil(duckDuckGoEngine.queryForSearchURL(googleSearchURL))
// check that if you pass in a nil URL that everything works
XCTAssertNil(duckDuckGoEngine.queryForSearchURL(nil))
}
fileprivate func startMockSuggestServer() -> String {
let webServer: GCDWebServer = GCDWebServer()
webServer.addHandler(forMethod: "GET", path: "/", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in
var suggestions: [String]!
let query = request?.query["q"] as! String
switch query {
case "foo":
suggestions = ["foo", "foo2", "foo you"]
case "foo bar":
suggestions = ["foo bar soap", "foo barstool", "foo bartender"]
default:
XCTFail("Unexpected query: \(query)")
}
return GCDWebServerDataResponse(jsonObject: [query, suggestions])
}
if !webServer.start(withPort: 0, bonjourName: nil) {
XCTFail("Can't start the GCDWebServer")
}
return "http://localhost:\(webServer.port)"
}
}

View file

@ -0,0 +1,74 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import XCTest
class StringExtensionsTests: XCTestCase {
func testStartsWith() {
XCTAssertTrue("abcde".startsWith("abcde"))
XCTAssertTrue("abcde".startsWith(""))
XCTAssertTrue("abcde".startsWith("a"))
XCTAssertTrue("abcdea".startsWith("a"))
XCTAssertFalse("abcde".startsWith("fa"))
XCTAssertFalse("abcde".startsWith("af"))
XCTAssertFalse("abcde".startsWith("b"))
}
func testEndsWith() {
XCTAssertTrue("abcde".endsWith("abcde"))
XCTAssertTrue("abcde".endsWith(""))
XCTAssertTrue("abcde".endsWith("e"))
XCTAssertTrue("abcdea".endsWith("a"))
XCTAssertFalse("abcde".endsWith("fe"))
XCTAssertFalse("abcde".endsWith("ef"))
XCTAssertFalse("abcde".endsWith("d"))
}
func testEllipsize() {
// Odd maxLength. Note that we ellipsize with a Unicode join character to avoid wrapping.
XCTAssertEqual("abcd…\u{2060}fgh", "abcdefgh".ellipsize(maxLength: 7))
// Even maxLength.
XCTAssertEqual("abcd…\u{2060}ijkl", "abcdefghijkl".ellipsize(maxLength: 8))
// String shorter than maxLength.
XCTAssertEqual("abcd", "abcd".ellipsize(maxLength: 7))
// Empty String.
XCTAssertEqual("", "".ellipsize(maxLength: 8))
// maxLength < 2.
XCTAssertEqual("abcdefgh", "abcdefgh".ellipsize(maxLength: 0))
}
func testStringByTrimmingLeadingCharactersInSet() {
XCTAssertEqual("foo ", " foo ".stringByTrimmingLeadingCharactersInSet(CharacterSet.whitespaces))
XCTAssertEqual("foo456", "123foo456".stringByTrimmingLeadingCharactersInSet(CharacterSet.decimalDigits))
XCTAssertEqual("", "123456".stringByTrimmingLeadingCharactersInSet(CharacterSet.decimalDigits))
}
func testStringSplitWithNewline() {
XCTAssertEqual("", "".stringSplitWithNewline())
XCTAssertEqual("foo", "foo".stringSplitWithNewline())
XCTAssertEqual("aaa\n bbb", "aaa bbb".stringSplitWithNewline())
XCTAssertEqual("Mark as\n Read", "Mark as Read".stringSplitWithNewline())
XCTAssertEqual("aa\n bbbbbb", "aa bbbbbb".stringSplitWithNewline())
}
func testPercentEscaping() {
func roundtripTest(_ input: String, _ expected: String, file: StaticString = #file, line: UInt = #line) {
let observed = input.escape()!
XCTAssertEqual(observed, expected, "input is \(input)", file: file, line: line)
let roundtrip = observed.unescape()
XCTAssertEqual(roundtrip, input, "encoded is \(observed)", file: file, line: line)
}
roundtripTest("https://mozilla.com", "https://mozilla.com")
roundtripTest("http://www.cnn.com/2017/09/25/politics/north-korea-fm-us-bombers/index.html", "http://www.cnn.com/2017/09/25/politics/north-korea-fm-us-bombers/index.html")
roundtripTest("http://mozilla.com/?a=foo&b=bar", "http://mozilla.com/%3Fa%3Dfoo%26b%3Dbar")
}
}

View file

@ -0,0 +1,109 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
@testable import Sync
import Shared
import Storage
import XCTest
private class RandomError: MaybeErrorType {
var description = "random_error"
}
class SyncStatusResolverTests: XCTestCase {
private func mockStatsForCollection(collection: String) -> SyncEngineStatsSession {
return SyncEngineStatsSession(collection: collection)
}
func testAllCompleted() {
let results: EngineResults = [
("tabs", .completed(mockStatsForCollection(collection: "tabs"))),
("clients", .completed(mockStatsForCollection(collection: "clients")))
]
let maybeResults = Maybe(success: results)
let resolver = SyncStatusResolver(engineResults: maybeResults)
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.good)
}
func testAllCompletedExceptOneDisabledRemotely() {
let results: EngineResults = [
("tabs", .completed(mockStatsForCollection(collection: "tabs"))),
("clients", .notStarted(.engineRemotelyNotEnabled(collection: "clients")))
]
let maybeResults = Maybe(success: results)
let resolver = SyncStatusResolver(engineResults: maybeResults)
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.good)
}
func testAllCompletedExceptNotStartedBecauseNoAccount() {
let results: EngineResults = [
("tabs", .completed(mockStatsForCollection(collection: "tabs"))),
("clients", .notStarted(.noAccount))
]
let maybeResults = Maybe(success: results)
let resolver = SyncStatusResolver(engineResults: maybeResults)
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.warning(message: Strings.FirefoxSyncOfflineTitle))
}
func testAllCompletedExceptNotStartedBecauseOffline() {
let results: EngineResults = [
("tabs", .completed(mockStatsForCollection(collection: "tabs"))),
("clients", .notStarted(.offline))
]
let maybeResults = Maybe(success: results)
let resolver = SyncStatusResolver(engineResults: maybeResults)
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.bad(message: Strings.FirefoxSyncOfflineTitle))
}
func testOfflineAndNoAccount() {
let results: EngineResults = [
("tabs", .notStarted(.noAccount)),
("clients", .notStarted(.offline))
]
let maybeResults = Maybe(success: results)
let resolver = SyncStatusResolver(engineResults: maybeResults)
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.bad(message: Strings.FirefoxSyncOfflineTitle))
}
func testAllPartial() {
let results: EngineResults = [
("tabs", .partial(SyncEngineStatsSession(collection: "tabs"))),
("clients", .partial(SyncEngineStatsSession(collection: "clients")))
]
let maybeResults = Maybe(success: results)
let resolver = SyncStatusResolver(engineResults: maybeResults)
XCTAssertTrue(resolver.resolveResults() == SyncDisplayState.good)
}
func testBookmarkMergeError() {
let maybeResults: Maybe<EngineResults> = Maybe(failure: BookmarksMergeError())
let resolver = SyncStatusResolver(engineResults: maybeResults)
let expected = SyncDisplayState.warning(message: String(format: Strings.FirefoxSyncPartialTitle, Strings.localizedStringForSyncComponent("bookmarks") ?? ""))
XCTAssertTrue(resolver.resolveResults() == expected)
}
func testBufferInvalidError() {
let maybeResults: Maybe<EngineResults> = Maybe(failure: BufferInvalidError(inconsistencies: [:], validationDuration: 0))
let resolver = SyncStatusResolver(engineResults: maybeResults)
let expected = SyncDisplayState.warning(message: String(format: Strings.FirefoxSyncPartialTitle, Strings.localizedStringForSyncComponent("bookmarks") ?? ""))
XCTAssertTrue(resolver.resolveResults() == expected)
}
func testRandomFailure() {
let maybeResults: Maybe<EngineResults> = Maybe(failure: RandomError())
let resolver = SyncStatusResolver(engineResults: maybeResults)
let expected = SyncDisplayState.bad(message: nil)
XCTAssertTrue(resolver.resolveResults() == expected)
}
}

View file

@ -0,0 +1,87 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
@testable import Client
import WebKit
import XCTest
class TabEventHandlerTests: XCTestCase {
func testEventDelivery() {
let tab = Tab(configuration: WKWebViewConfiguration())
let handler = DummyHandler()
XCTAssertNil(handler.isFocused)
TabEvent.post(.didGainFocus, for: tab)
XCTAssertTrue(handler.isFocused!)
TabEvent.post(.didLoseFocus, for: tab)
XCTAssertFalse(handler.isFocused!)
}
func testUnregistration() {
let tab = Tab(configuration: WKWebViewConfiguration())
let handler = DummyHandler()
XCTAssertNil(handler.isFocused)
TabEvent.post(.didGainFocus, for: tab)
XCTAssertTrue(handler.isFocused!)
handler.doUnregister()
TabEvent.post(.didLoseFocus, for: tab)
// The event didn't reach us, so we should still be focused.
XCTAssertTrue(handler.isFocused!)
}
func testOnlyRegisteredForEvents() {
let tab = Tab(configuration: WKWebViewConfiguration())
let handler = DummyHandler()
handler.doUnregister()
let tabObservers = handler.registerFor(.didGainFocus)
XCTAssertNil(handler.isFocused)
TabEvent.post(.didGainFocus, for: tab)
XCTAssertTrue(handler.isFocused!)
TabEvent.post(.didLoseFocus, for: tab)
XCTAssertTrue(handler.isFocused!)
handler.unregister(tabObservers)
}
}
class DummyHandler: TabEventHandler {
var tabObservers: TabObservers!
// This is not how this should be written in production the handler shouldn't be keeping track
// of individual tab state.
var isFocused: Bool? = nil
init() {
tabObservers = registerFor(.didGainFocus, .didLoseFocus)
}
deinit {
doUnregister()
}
fileprivate func doUnregister() {
unregister(tabObservers)
}
func tabDidGainFocus(_ tab: Tab) {
isFocused = true
}
func tabDidLoseFocus(_ tab: Tab) {
isFocused = false
}
}

View file

@ -0,0 +1,457 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import Shared
import Storage
import UIKit
import WebKit
import Deferred
import XCTest
open class TabManagerMockProfile: MockProfile {
var numberOfTabsStored = 0
override public func storeTabs(_ tabs: [RemoteTab]) -> Deferred<Maybe<Int>> {
numberOfTabsStored = tabs.count
return deferMaybe(tabs.count)
}
}
open class MockTabManagerStateDelegate: TabManagerStateDelegate {
var numberOfTabsStored = 0
public func tabManagerWillStoreTabs(_ tabs: [Tab]) {
numberOfTabsStored = tabs.count
}
}
struct MethodSpy {
let functionName: String
let method: ((_ tabs: [Tab?]) -> Void)?
init(functionName: String) {
self.functionName = functionName
self.method = nil
}
init(functionName: String, method: ((_ tabs: [Tab?]) -> Void)?) {
self.functionName = functionName
self.method = method
}
}
open class MockTabManagerDelegate: TabManagerDelegate {
//this array represents the order in which delegate methods should be called.
//each delegate method will pop the first struct from the array. If the method name doesn't match the struct then the order is incorrect
//Then it evaluates the method closure which will return true/false depending on if the tabs are correct
var methodCatchers: [MethodSpy] = []
func expect(_ methods: [MethodSpy]) {
self.methodCatchers = methods
}
func verify(_ message: String) {
XCTAssertTrue(methodCatchers.isEmpty, message)
}
func testDelegateMethodWithName(_ name: String, tabs: [Tab?]) {
guard let spy = self.methodCatchers.first else {
XCTAssert(false, "No method was availible in the queue. For the delegate method \(name) to use")
return
}
XCTAssertEqual(spy.functionName, name)
if let methodCheck = spy.method {
methodCheck(tabs)
}
methodCatchers.removeFirst()
}
public func tabManager(_ tabManager: TabManager, didSelectedTabChange selected: Tab?, previous: Tab?) {
testDelegateMethodWithName(#function, tabs: [selected, previous])
}
public func tabManager(_ tabManager: TabManager, didAddTab tab: Tab) {
testDelegateMethodWithName(#function, tabs: [tab])
}
public func tabManager(_ tabManager: TabManager, didRemoveTab tab: Tab) {
testDelegateMethodWithName(#function, tabs: [tab])
}
public func tabManagerDidRestoreTabs(_ tabManager: TabManager) {
testDelegateMethodWithName(#function, tabs: [])
}
public func tabManager(_ tabManager: TabManager, willRemoveTab tab: Tab) {
testDelegateMethodWithName(#function, tabs: [tab])
}
public func tabManager(_ tabManager: TabManager, willAddTab tab: Tab) {
testDelegateMethodWithName(#function, tabs: [tab])
}
public func tabManagerDidAddTabs(_ tabManager: TabManager) {
testDelegateMethodWithName(#function, tabs: [])
}
public func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) {
testDelegateMethodWithName(#function, tabs: [])
}
}
class TabManagerTests: XCTestCase {
let willRemove = MethodSpy(functionName: "tabManager(_:willRemoveTab:)")
let didRemove = MethodSpy(functionName: "tabManager(_:didRemoveTab:)")
let willAdd = MethodSpy(functionName: "tabManager(_:willAddTab:)")
let didAdd = MethodSpy(functionName: "tabManager(_:didAddTab:)")
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func testTabManagerCallsTabManagerStateDelegateOnStoreChangesWithNormalTabs() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let stateDelegate = MockTabManagerStateDelegate()
manager.stateDelegate = stateDelegate
let configuration = WKWebViewConfiguration()
configuration.processPool = WKProcessPool()
// test that non-private tabs are saved to the db
// add some non-private tabs to the tab manager
for _ in 0..<3 {
let tab = Tab(configuration: configuration)
tab.url = URL(string: "http://yahoo.com")!
manager.configureTab(tab, request: URLRequest(url: tab.url!), flushToDisk: false, zombie: false)
}
manager.storeChanges()
XCTAssertEqual(stateDelegate.numberOfTabsStored, 3, "Expected state delegate to have been called with 3 tabs, but called with \(stateDelegate.numberOfTabsStored)")
}
func testTabManagerDoesNotCallTabManagerStateDelegateOnStoreChangesWithPrivateTabs() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let stateDelegate = MockTabManagerStateDelegate()
manager.stateDelegate = stateDelegate
let configuration = WKWebViewConfiguration()
configuration.processPool = WKProcessPool()
// test that non-private tabs are saved to the db
// add some non-private tabs to the tab manager
for _ in 0..<3 {
let tab = Tab(configuration: configuration, isPrivate: true)
tab.url = URL(string: "http://yahoo.com")!
manager.configureTab(tab, request: URLRequest(url: tab.url!), flushToDisk: false, zombie: false)
}
manager.storeChanges()
XCTAssertEqual(stateDelegate.numberOfTabsStored, 0, "Expected state delegate to have been called with 3 tabs, but called with \(stateDelegate.numberOfTabsStored)")
}
func testAddTab() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let delegate = MockTabManagerDelegate()
manager.addDelegate(delegate)
delegate.expect([willAdd, didAdd])
manager.addTab()
delegate.verify("Not all delegate methods were called")
}
func testDidDeleteLastTab() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let delegate = MockTabManagerDelegate()
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
let tab = manager.addTab()
manager.selectTab(tab)
manager.addDelegate(delegate)
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
let next = tabs[0]!
let previous = tabs[1]!
XCTAssertTrue(previous != next)
XCTAssertTrue(previous == tab)
XCTAssertFalse(next.isPrivate)
}
delegate.expect([willRemove, didRemove, willAdd, didAdd, didSelect])
manager.removeTab(tab)
delegate.verify("Not all delegate methods were called")
}
func testDidDeleteLastPrivateTab() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let delegate = MockTabManagerDelegate()
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
let tab = manager.addTab()
manager.selectTab(tab)
let privateTab = manager.addTab(isPrivate: true)
manager.selectTab(privateTab)
manager.addDelegate(delegate)
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
let next = tabs[0]!
let previous = tabs[1]!
XCTAssertTrue(previous != next)
XCTAssertTrue(previous == privateTab)
XCTAssertTrue(next == tab)
XCTAssertTrue(previous.isPrivate)
XCTAssertTrue(manager.selectedTab == next)
}
delegate.expect([willRemove, didRemove, didSelect])
manager.removeTab(privateTab)
delegate.verify("Not all delegate methods were called")
}
func testDeletePrivateTabsOnExit() {
//setup
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
profile.prefs.setBool(true, forKey: "settings.closePrivateTabs")
// create one private and one normal tab
let tab = manager.addTab()
manager.selectTab(tab)
manager.selectTab(manager.addTab(isPrivate: true))
XCTAssertEqual(manager.selectedTab?.isPrivate, true, "The selected tab should be the private tab")
XCTAssertEqual(manager.privateTabs.count, 1, "There should only be one private tab")
manager.selectTab(tab)
XCTAssertEqual(manager.privateTabs.count, 0, "If the normal tab is selected the private tab should have been deleted")
XCTAssertEqual(manager.normalTabs.count, 1, "The regular tab should stil be around")
manager.selectTab(manager.addTab(isPrivate: true))
XCTAssertEqual(manager.privateTabs.count, 1, "There should be one new private tab")
manager.willSwitchTabMode()
XCTAssertEqual(manager.privateTabs.count, 0, "After willSwitchTabMode there should be no more private tabs")
manager.selectTab(manager.addTab(isPrivate: true))
manager.selectTab(manager.addTab(isPrivate: true))
XCTAssertEqual(manager.privateTabs.count, 2, "Private tabs should not be deleted when another one is added")
manager.selectTab(manager.addTab())
XCTAssertEqual(manager.privateTabs.count, 0, "But once we add a normal tab we've switched out of private mode. Private tabs should be deleted")
XCTAssertEqual(manager.normalTabs.count, 2, "The original normal tab and the new one should both still exist")
profile.prefs.setBool(false, forKey: "settings.closePrivateTabs")
manager.selectTab(manager.addTab(isPrivate: true))
manager.selectTab(tab)
XCTAssertEqual(manager.selectedTab?.isPrivate, false, "The selected tab should not be private")
XCTAssertEqual(manager.privateTabs.count, 1, "If the flag is false then private tabs should still exist")
}
func testDeleteNonSelectedTab() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let delegate = MockTabManagerDelegate()
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
let tab = manager.addTab()
manager.selectTab(tab)
manager.addTab()
let deleteTab = manager.addTab()
manager.addDelegate(delegate)
delegate.expect([willRemove, didRemove])
manager.removeTab(deleteTab)
delegate.verify("Not all delegate methods were called")
}
func testDeleteSelectedTab() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let delegate = MockTabManagerDelegate()
func addTab(_ load: Bool) -> Tab {
let tab = manager.addTab()
if load {
tab.lastExecutedTime = Date.now()
}
return tab
}
let tab0 = addTab(false) // not loaded
let tab1 = addTab(true)
let tab2 = addTab(true)
let tab3 = addTab(false) // not loaded
let tab4 = addTab(true)
// starting at tab2, we should be selecting
// [ tab4, tab1, tab3, tab0 ]
manager.selectTab(tab2)
manager.removeTab(manager.selectedTab!)
// Rule: most recently loaded.
XCTAssertEqual(manager.selectedTab, tab4)
manager.removeTab(manager.selectedTab!)
// Rule: most recently loaded.
XCTAssertEqual(manager.selectedTab, tab1)
manager.removeTab(manager.selectedTab!)
// Rule: next to the right.
XCTAssertEqual(manager.selectedTab, tab3)
manager.removeTab(manager.selectedTab!)
// Rule: last one left.
XCTAssertEqual(manager.selectedTab, tab0)
}
func testDeleteLastTab() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let delegate = MockTabManagerDelegate()
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
(0..<10).forEach {_ in manager.addTab() }
manager.selectTab(manager.tabs.last)
let deleteTab = manager.tabs.last
let newSelectedTab = manager.tabs[8]
manager.addDelegate(delegate)
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
let next = tabs[0]!
let previous = tabs[1]!
XCTAssertEqual(deleteTab, previous)
XCTAssertEqual(next, newSelectedTab)
}
delegate.expect([willRemove, didRemove, didSelect])
manager.removeTab(manager.tabs.last!)
delegate.verify("Not all delegate methods were called")
}
func testDelegatesCalledWhenRemovingPrivateTabs() {
//setup
let profile = TabManagerMockProfile()
let delegate = MockTabManagerDelegate()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
profile.prefs.setBool(true, forKey: "settings.closePrivateTabs")
// create one private and one normal tab
let tab = manager.addTab()
let newTab = manager.addTab()
manager.selectTab(tab)
manager.selectTab(manager.addTab(isPrivate: true))
manager.addDelegate(delegate)
// Double check a few things
XCTAssertEqual(manager.selectedTab?.isPrivate, true, "The selected tab should be the private tab")
XCTAssertEqual(manager.privateTabs.count, 1, "There should only be one private tab")
// switch to normal mode. Which should delete the private tabs
manager.willSwitchTabMode()
//make sure tabs are cleared properly and indexes are reset
XCTAssertEqual(manager.privateTabs.count, 0, "Private tab should have been deleted")
XCTAssertEqual(manager.selectedIndex, -1, "The selected index should have been reset")
// didSelect should still be called when switching between a nil tab
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
XCTAssertNil(tabs[1], "there should be no previous tab")
let next = tabs[0]!
XCTAssertFalse(next.isPrivate)
}
// make sure delegate method is actually called
delegate.expect([didSelect])
// select the new tab to trigger the delegate methods
manager.selectTab(newTab)
// check
delegate.verify("Not all delegate methods were called")
}
func testDeleteFirstTab() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let delegate = MockTabManagerDelegate()
//create the tab before adding the mock delegate. So we don't have to check delegate calls we dont care about
(0..<10).forEach {_ in manager.addTab() }
manager.selectTab(manager.tabs.first)
let deleteTab = manager.tabs.first
let newSelectedTab = manager.tabs[1]
manager.addDelegate(delegate)
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
let next = tabs[0]!
let previous = tabs[1]!
XCTAssertEqual(deleteTab, previous)
XCTAssertEqual(next, newSelectedTab)
}
delegate.expect([willRemove, didRemove, didSelect])
manager.removeTab(manager.tabs.first!)
delegate.verify("Not all delegate methods were called")
}
// Private tabs and regular tabs are in the same tabs array.
// Make sure that when a private tab is added inbetween regular tabs it isnt accidently selected when removing a regular tab
func testTabsIndex() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let delegate = MockTabManagerDelegate()
// We add 2 tabs. Then a private one before adding another normal tab and selecting it.
// Make sure that when the last one is deleted we dont switch to the private tab
manager.addTab()
let newSelected = manager.addTab()
manager.addTab(isPrivate: true)
let deleted = manager.addTab()
manager.selectTab(manager.tabs.last)
manager.addDelegate(delegate)
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
let next = tabs[0]!
let previous = tabs[1]!
XCTAssertEqual(deleted, previous)
XCTAssertEqual(next, newSelected)
}
delegate.expect([willRemove, didRemove, didSelect])
manager.removeTab(manager.tabs.last!)
delegate.verify("Not all delegate methods were called")
}
func testTabsIndexClosingFirst() {
let profile = TabManagerMockProfile()
let manager = TabManager(prefs: profile.prefs, imageStore: nil)
let delegate = MockTabManagerDelegate()
// We add 2 tabs. Then a private one before adding another normal tab and selecting the first.
// Make sure that when the last one is deleted we dont switch to the private tab
let deleted = manager.addTab()
let newSelected = manager.addTab()
manager.addTab(isPrivate: true)
manager.addTab()
manager.selectTab(manager.tabs.first)
manager.addDelegate(delegate)
let didSelect = MethodSpy(functionName: "tabManager(_:didSelectedTabChange:previous:)") { tabs in
let next = tabs[0]!
let previous = tabs[1]!
XCTAssertEqual(deleted, previous)
XCTAssertEqual(next, newSelected)
}
delegate.expect([willRemove, didRemove, didSelect])
manager.removeTab(manager.tabs.first!)
delegate.verify("Not all delegate methods were called")
}
}

View file

@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import Foundation
import Shared
import Storage
import XCTest
class TestBookmarks: ProfileTest {
func testBookmarks() {
withTestProfile { profile -> Void in
for i in 0...10 {
let bookmark = ShareItem(url: "http://www.example.com/\(i)", title: "Example \(i)", favicon: nil)
profile.bookmarks.shareItem(bookmark)
}
let expectation = self.expectation(description: "asynchronous request")
profile.bookmarks.modelFactory >>== {
$0.modelForFolder(BookmarkRoots.MobileFolderGUID).upon { result in
guard let model = result.successValue else {
XCTFail("Should not have failed to get mock bookmarks.")
expectation.fulfill()
return
}
// 11 bookmarks plus our two suggested sites.
XCTAssertEqual(model.current.count, 11, "We create \(model.current.count) stub bookmarks in the Mobile Bookmarks folder.")
let bookmark = model.current[0]
XCTAssertTrue(bookmark is BookmarkItem)
XCTAssertTrue((bookmark as! BookmarkItem).url.hasPrefix("http://www.example.com/"), "Example URL found.")
expectation.fulfill()
}
}
self.waitForExpectations(timeout: 10.0, handler:nil)
// This'll do.
try! profile.files.remove("mock.db")
}
}
}

View file

@ -0,0 +1,57 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import XCTest
import Storage
import SDWebImage
@testable import Client
import Shared
class TestFavicons: ProfileTest {
fileprivate func addSite(_ favicons: Favicons, url: String, s: Bool = true) {
let expectation = self.expectation(description: "Wait for history")
let site = Site(url: url, title: "")
let icon = Favicon(url: url + "/icon.png", type: IconType.icon)
favicons.addFavicon(icon, forSite: site).upon {
XCTAssertEqual($0.isSuccess, s, "Icon added \(url)")
expectation.fulfill()
}
self.waitForExpectations(timeout: 100, handler: nil)
}
func testFaviconFetcherParse() {
let expectation = self.expectation(description: "Wait for Favicons to be fetched")
let profile = MockProfile()
// I want a site that also has an iOS app so I can get "apple-touch-icon-precomposed" icons as well
let url = URL(string: "https://instagram.com")
FaviconFetcher.getForURL(url!, profile: profile).uponQueue(DispatchQueue.main) { result in
guard let favicons = result.successValue, favicons.count > 0, let url = favicons.first?.url.asURL else {
XCTFail("Favicons were not found.")
return expectation.fulfill()
}
XCTAssertGreaterThan(favicons.count, 1, "Instagram should have more than one Favicon.")
SDWebImageManager.shared().loadImage(with: url, options: .retryFailed, progress: nil, completed: { (img, _, _, _, _, _) in
guard let image = img else {
XCTFail("Not a valid URL provided for a favicon.")
return expectation.fulfill()
}
XCTAssertNotEqual(image.size, CGSize(width: 0, height: 0))
expectation.fulfill()
})
}
self.waitForExpectations(timeout: 3000, handler: nil)
}
func testDefaultFavicons() {
let icon = FaviconFetcher.getDefaultIconForURL(url: URL(string: "http://www.google.de")!)
XCTAssertNotNil(icon)
let craigsListIcon = FaviconFetcher.getDefaultIconForURL(url: URL(string: "http://vancouver.craigslist.ca")!)
XCTAssertNotNil(craigsListIcon)
}
}

View file

@ -0,0 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import XCTest
class TestHashExtensions: XCTestCase {
func testSha1() {
XCTAssertEqual("test1test2".sha1.hexEncodedString, "dff964f6e3c1761b6288f5c75c319d36fb09b2b9")
XCTAssertEqual("test2test3".sha1.hexEncodedString, "66cdfcbbf4ad73f40ae06140460ff9bb0aabaf5c")
}
func testSha256() {
let data1: Data = "4f980b6f9baa6965f760d0bf2b2ccbee483032e5df01d77bbd9e25f7517a06b9".hexDecodedData
XCTAssertEqual("test1test2".sha256, data1)
XCTAssertEqual("test2test3".sha256, "fc3ea28dc1801e4180cec1022b55bee7795cf3c9fd430fb5237c9d8054218e81".hexDecodedData)
}
}

View file

@ -0,0 +1,219 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@testable import Client
import Foundation
import Storage
import XCTest
class TestHistory: ProfileTest {
fileprivate func addSite(_ history: BrowserHistory, url: String, title: String, s: Bool = true) {
let site = Site(url: url, title: title)
let visit = SiteVisit(site: site, date: Date.nowMicroseconds())
XCTAssertEqual(s, history.addLocalVisit(visit).value.isSuccess, "Site added: \(url).")
}
fileprivate func innerCheckSites(_ history: BrowserHistory, callback: @escaping (_ cursor: Cursor<Site>) -> Void) {
// Retrieve the entry
history.getSitesByLastVisit(100).upon {
XCTAssertTrue($0.isSuccess)
callback($0.successValue!)
}
}
fileprivate func checkSites(_ history: BrowserHistory, urls: [String: String], s: Bool = true) {
// Retrieve the entry.
if let cursor = history.getSitesByLastVisit(100).value.successValue {
XCTAssertEqual(cursor.status, CursorStatus.success, "Returned success \(cursor.statusMessage).")
XCTAssertEqual(cursor.count, urls.count, "Cursor has \(urls.count) entries.")
for index in 0..<cursor.count {
let s = cursor[index]!
XCTAssertNotNil(s, "Cursor has a site for entry.")
let title = urls[s.url]
XCTAssertNotNil(title, "Found right URL.")
XCTAssertEqual(s.title, title!, "Found right title.")
}
} else {
XCTFail("Couldn't get cursor.")
}
}
fileprivate func clear(_ history: BrowserHistory) {
XCTAssertTrue(history.clearHistory().value.isSuccess, "History cleared.")
}
fileprivate func checkVisits(_ history: BrowserHistory, url: String) {
let expectation = self.expectation(description: "Wait for history")
history.getSitesByLastVisit(100).upon { result in
XCTAssertTrue(result.isSuccess)
history.getFrecentHistory().getSites(whereURLContains: url, historyLimit: 100, bookmarksLimit: 0).upon { result in
XCTAssertTrue(result.isSuccess)
let cursor = result.successValue!
XCTAssertEqual(cursor.status, CursorStatus.success, "returned success \(cursor.statusMessage)")
// XXX - We don't allow querying much info about visits here anymore, so there isn't a lot to do
expectation.fulfill()
}
}
self.waitForExpectations(timeout: 100, handler: nil)
}
// This is a very basic test. Adds an entry. Retrieves it, and then clears the database
func testHistory() {
withTestProfile { profile -> Void in
let h = profile.history
self.addSite(h, url: "http://url1/", title: "title")
self.addSite(h, url: "http://url1/", title: "title")
self.addSite(h, url: "http://url1/", title: "title 2")
self.addSite(h, url: "https://url2/", title: "title")
self.addSite(h, url: "https://url2/", title: "title")
self.checkSites(h, urls: ["http://url1/": "title 2", "https://url2/": "title"])
self.checkVisits(h, url: "http://url1/")
self.checkVisits(h, url: "https://url2/")
self.clear(h)
}
}
func testAboutUrls() {
withTestProfile { (profile) -> Void in
let h = profile.history
self.addSite(h, url: "about:home", title: "About Home", s: false)
self.clear(h)
}
}
let NumThreads = 5
let NumCmds = 10
func testInsertPerformance() {
withTestProfile { profile -> Void in
let h = profile.history
var j = 0
self.measure({ () -> Void in
for _ in 0...self.NumCmds {
self.addSite(h, url: "https://someurl\(j).com/", title: "title \(j)")
j += 1
}
self.clear(h)
})
}
}
func testGetPerformance() {
withTestProfile { profile -> Void in
let h = profile.history
var j = 0
var urls = [String: String]()
self.clear(h)
for _ in 0...self.NumCmds {
self.addSite(h, url: "https://someurl\(j).com/", title: "title \(j)")
urls["https://someurl\(j).com/"] = "title \(j)"
j += 1
}
self.measure({ () -> Void in
self.checkSites(h, urls: urls)
return
})
self.clear(h)
}
}
// Fuzzing tests. These fire random insert/query/clear commands into the history database from threads. The don't check
// the results. Just look for crashes.
func testRandomThreading() {
withTestProfile { profile -> Void in
let queue = DispatchQueue(label: "My Queue", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.inherit, target: nil)
var counter = 0
let expectation = self.expectation(description: "Wait for history")
for _ in 0..<self.NumThreads {
var history = profile.history as BrowserHistory
self.runRandom(&history, queue: queue, cb: { () -> Void in
counter += 1
if counter == self.NumThreads {
self.clear(history)
expectation.fulfill()
}
})
}
self.waitForExpectations(timeout: 10, handler: nil)
}
}
// Same as testRandomThreading, but uses one history connection for all threads
func testRandomThreading2() {
withTestProfile { profile -> Void in
let queue = DispatchQueue(label: "My Queue", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.inherit, target: nil)
var history = profile.history as BrowserHistory
var counter = 0
let expectation = self.expectation(description: "Wait for history")
for _ in 0..<self.NumThreads {
self.runRandom(&history, queue: queue, cb: { () -> Void in
counter += 1
if counter == self.NumThreads {
self.clear(history)
expectation.fulfill()
}
})
}
self.waitForExpectations(timeout: 10, handler: nil)
}
}
// Runs a random command on a database. Calls cb when finished.
fileprivate func runRandom(_ history: inout BrowserHistory, cmdIn: Int, cb: @escaping () -> Void) {
var cmd = cmdIn
if cmd < 0 {
cmd = Int(arc4random() % 5)
}
switch cmd {
case 0...1:
let url = "https://randomurl.com/\(arc4random() % 100)"
let title = "title \(arc4random() % 100)"
addSite(history, url: url, title: title)
cb()
case 2...3:
innerCheckSites(history) { cursor in
for site in cursor {
_ = site!
}
}
cb()
default:
history.clearHistory().upon() { success in cb() }
}
}
// Calls numCmds random methods on this database. val is a counter used by this interally (i.e. always pass zero for it).
// Calls cb when finished.
fileprivate func runMultiRandom(_ history: inout BrowserHistory, val: Int, numCmds: Int, cb: @escaping () -> Void) {
if val == numCmds {
cb()
return
} else {
runRandom(&history, cmdIn: -1) { [history]_ in
var history = history
self.runMultiRandom(&history, val: val+1, numCmds: numCmds, cb: cb)
}
}
}
// Helper for starting a new thread running NumCmds random methods on it. Calls cb when done.
fileprivate func runRandom(_ history: inout BrowserHistory, queue: DispatchQueue, cb: @escaping () -> Void) {
queue.async { [history] in
var history = history
// Each thread creates its own history provider
self.runMultiRandom(&history, val: 0, numCmds: self.NumCmds) { _ in
DispatchQueue.main.async(execute: cb)
}
}
}
}

View file

@ -0,0 +1,94 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import UIKit
import XCTest
import Storage
import SDWebImage
import GCDWebServers
import Shared
@testable import Client
class UIImageViewExtensionsTests: XCTestCase {
override func setUp() {
SDWebImageDownloader.shared().urlCredential = WebServer.sharedInstance.credentials
}
func testsetIcon() {
let url = URL(string: "http://mozilla.com")
let imageView = UIImageView()
let goodIcon = FaviconFetcher.getDefaultFavicon(url!)
let correctColor = FaviconFetcher.getDefaultColor(url!)
imageView.setIcon(nil, forURL: url)
XCTAssertEqual(imageView.image!, goodIcon, "The correct default favicon should be applied")
XCTAssertEqual(imageView.backgroundColor, correctColor, "The correct default color should be applied")
imageView.setIcon(nil, forURL: URL(string: "http://mozilla.com/blahblah"))
XCTAssertEqual(imageView.image!, goodIcon, "The same icon should be applied to all urls with the same domain")
imageView.setIcon(nil, forURL: URL(string: "b"))
XCTAssertEqual(imageView.image, FaviconFetcher.defaultFavicon, "The default favicon should be applied when no information is given about the icon")
}
func testAsyncSetIcon() {
let originalImage = UIImage(named: "fxLogo")!
WebServer.sharedInstance.registerHandlerForMethod("GET", module: "favicon", resource: "icon") { (request) -> GCDWebServerResponse! in
return GCDWebServerDataResponse(data: UIImagePNGRepresentation(originalImage), contentType: "image/png")
}
let favImageView = UIImageView()
favImageView.setIcon(Favicon(url: "http://localhost:6571/favicon/icon", type: .guess), forURL: URL(string: "http://localhost:6571"))
let expect = expectation(description: "UIImageView async load")
let time = Int64(2 * Double(NSEC_PER_SEC))
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(time) / Double(NSEC_PER_SEC)) {
XCTAssert(originalImage.isStrictlyEqual(to: favImageView.image!), "The correct favicon should be applied to the UIImageView")
expect.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
}
func testAsyncSetIconFail() {
let favImageView = UIImageView()
let gFavURL = URL(string: "https://www.nofavicon.com/noicon.ico")
let gURL = URL(string: "http://nofavicon.com")
let correctImage = FaviconFetcher.getDefaultFavicon(gURL!)
favImageView.setIcon(Favicon(url: gFavURL!.absoluteString, type: .guess), forURL: gURL)
let expect = expectation(description: "UIImageView async load")
let time = Int64(2 * Double(NSEC_PER_SEC))
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(time) / Double(NSEC_PER_SEC)) {
XCTAssert(correctImage.isStrictlyEqual(to: favImageView.image!), "The correct default favicon should be applied to the UIImageView")
expect.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
}
func testDefaultIcons() {
let favImageView = UIImageView()
let gFavURL = URL(string: "https://www.facebook.com/fav") //This will be fetched from tippy top sites
let gURL = URL(string: "http://www.facebook.com")!
let defaultItem = FaviconFetcher.defaultIcons[gURL.baseDomain!]!
let correctImage = UIImage(contentsOfFile: defaultItem.url)!
favImageView.setIcon(Favicon(url: gFavURL!.absoluteString, type: .guess), forURL: gURL)
let expect = expectation(description: "UIImageView async load")
let time = Int64(2 * Double(NSEC_PER_SEC))
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(time) / Double(NSEC_PER_SEC)) {
XCTAssertEqual(favImageView.backgroundColor, defaultItem.color)
XCTAssert(correctImage.isStrictlyEqual(to: favImageView.image!), "The correct default favicon should be applied to the UIImageView")
expect.fulfill()
}
waitForExpectations(timeout: 5, handler: nil)
}
}

View file

@ -0,0 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import MobileCoreServices
import UIKit
import XCTest
class UIPasteboardExtensionsTests: XCTestCase {
fileprivate var pasteboard: UIPasteboard!
override func setUp() {
super.setUp()
pasteboard = UIPasteboard.withUniqueName()
}
override func tearDown() {
super.tearDown()
UIPasteboard.remove(withName: pasteboard.name)
}
func testAddPNGImage() {
let path = Bundle(for: self.classForCoder).path(forResource: "image", ofType: "png")!
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
let url = URL(string: "http://foo.bar")!
pasteboard.addImageWithData(data, forURL: url)
verifyPasteboard(expectedURL: url, expectedImageTypeKey: kUTTypePNG)
}
func testAddGIFImage() {
let path = Bundle(for: self.classForCoder).path(forResource: "image", ofType: "gif")!
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
let url = URL(string: "http://foo.bar")!
pasteboard.addImageWithData(data, forURL: url)
verifyPasteboard(expectedURL: url, expectedImageTypeKey: kUTTypeGIF)
}
fileprivate func verifyPasteboard(expectedURL: URL, expectedImageTypeKey: CFString) {
XCTAssertEqual(pasteboard.items.count, 1)
XCTAssertEqual(pasteboard.items[0].count, 2)
XCTAssertEqual(pasteboard.items[0][kUTTypeURL as String] as? URL, expectedURL)
XCTAssertNotNil(pasteboard.items[0][expectedImageTypeKey as String])
}
}

View file

@ -0,0 +1,49 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import UIKit
import GCDWebServers
import XCTest
/// Minimal web server tests. This class can be used as a base class for tests that need a real web server.
/// Simply add additional handlers your test class' setUp() method.
class WebServerTests: XCTestCase {
let webServer: GCDWebServer = GCDWebServer()
var webServerBase: String!
/// Setup a basic web server that binds to a random port and that has one default handler on /hello
fileprivate func setupWebServer() {
webServer.addHandler(forMethod: "GET", path: "/hello", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in
return GCDWebServerDataResponse(html: "<html><body><p>Hello World</p></body></html>")
}
if webServer.start(withPort: 0, bonjourName: nil) == false {
XCTFail("Can't start the GCDWebServer")
}
webServerBase = "http://localhost:\(webServer.port)"
}
override func setUp() {
super.setUp()
setupWebServer()
}
override func tearDown() {
super.tearDown()
}
func testWebServerIsRunning() {
XCTAssertTrue(webServer.isRunning)
}
func testWebServerIsServingRequests() {
let response: NSString?
do {
response = try NSString(contentsOf: URL(string: "\(webServerBase!)/hello")!, encoding: String.Encoding.utf8.rawValue)
} catch _ {
response = nil
}
XCTAssertNotNil(response)
XCTAssertTrue(response == "<html><body><p>Hello World</p></body></html>")
}
}

View file

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import XCTest
extension XCTestCase {
func wait(_ time: TimeInterval) {
let expectation = self.expectation(description: "Wait")
let delayTime = DispatchTime.now() + Double(Int64(time * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: delayTime) {
expectation.fulfill()
}
waitForExpectations(timeout: time + 1, handler: nil)
}
func waitForCondition(timeout: TimeInterval = 10, condition: () -> Bool) {
let timeoutTime = Date.timeIntervalSinceReferenceDate + timeout
while !condition() {
if Date.timeIntervalSinceReferenceDate > timeoutTime {
XCTFail("Condition timed out")
return
}
wait(0.1)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

View file

@ -0,0 +1,24 @@
{
"status": 1,
"list": [{
"id": 2092,
"url": "https:\/\/pocket.co\/xMnD5u",
"dedupe_url": "https:\/\/www.wired.com\/story\/turn-off-your-push-notifications\/",
"title": "Turn Off Your Push Notifications. All of Them",
"excerpt": "Push notifications are ruining my life. Yours too, I bet. Download more than a few apps and the notifications become a non-stop, cacophonous waterfall of nonsense. Here's just part of an afternoon on my phone:",
"domain": "wired.com",
"image_src": "https:\/\/d33ypg4xwx0n86.cloudfront.net\/direct?url=https%3A%2F%2Fmedia.wired.com%2Fphotos%2F597267fd023c38366e1ae497%2Fmaster%2Fw_1200%2Cc_limit%2Fno_notifications-TA.gif&resize=w450",
"published_timestamp": "1332306000",
"sort_id": 0
}, {
"id": 2091,
"url": "https:\/\/pocket.co\/sMnD5m",
"dedupe_url": "http:\/\/www.latimes.com\/business\/la-fi-agenda-best-buy-20170717-htmlstory.html",
"title": "Why the grim reaper of retail hasn't come to claim Best Buy",
"excerpt": "Five years ago Best Buy Co. looked like a retail dinosaur, another victim of e-commerce juggernaut Amazon.com and other online sellers.",
"domain": "latimes.com",
"image_src": "https:\/\/d33ypg4xwx0n86.cloudfront.net\/direct?url=http%3A%2F%2Fwww.trbimg.com%2Fimg-547cc080%2Fturbine%2Fla-fi-mh-radio-shack-20141201-004%2F&resize=w450",
"published_timestamp": "1500267600",
"sort_id": 1
}]
}