Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

483 lines
24 KiB
Swift

@testable import Wikipedia
@testable import WMF
import XCTest
import Foundation
class BasicCachingWebViewController: UIViewController, WKNavigationDelegate {
static let webProcessPool = WKProcessPool()
let schemeHandler: SchemeHandler
var articleURL = URL(string: "app://en.wikipedia.org/api/rest_v1/page/mobile-html/United_States")!
let session: Session
let configuration: Configuration
let permanentCache: PermanentCacheController
var didReceiveDataCallback: ((WKURLSchemeTask, Data) -> Void)?
var extraHeaders: [String: String] = [:]
init() {
self.permanentCache = ArticleTestHelpers.cacheController
self.session = ArticleTestHelpers.dataStore.session
self.configuration = ArticleTestHelpers.dataStore.configuration
self.schemeHandler = SchemeHandler(scheme: "app", session: session)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal lazy var fetcher: ArticleFetcher = ArticleFetcher(session: session, configuration: configuration)
lazy var webViewConfiguration: WKWebViewConfiguration = {
let configuration = WKWebViewConfiguration()
configuration.processPool = BasicCachingWebViewController.webProcessPool
configuration.setURLSchemeHandler(schemeHandler, forURLScheme: schemeHandler.scheme)
return configuration
}()
lazy var webView: WKWebView = {
return WMFWebView(frame: view.bounds, configuration: webViewConfiguration)
}()
override func viewDidLoad() {
super.viewDidLoad()
setupWebView()
if let request = getRequest() {
webView.load(request)
}
}
private func getRequest() -> URLRequest? {
let acceptUTF8HTML = ["Accept": "text/html; charset=utf-8"]
for (key, value) in acceptUTF8HTML {
extraHeaders[key] = value
}
return fetcher.urlRequest(from: articleURL, headers: extraHeaders)
}
private func setupWebView() {
schemeHandler.didReceiveDataCallback = self.didReceiveDataCallback
webView.navigationDelegate = self
view.wmf_addSubviewWithConstraintsToEdges(webView)
}
}
class ArticleTestHelpers {
struct FixtureData {
let image: Data
let html: Data
let css: Data
let cachedHTML: Data
let cachedCSS: Data
let cachedZhansHTML: Data
let cachedZhCSS: Data
let imageZh640: Data
}
static var fixtureData: FixtureData?
static let dataStore = MWKDataStore.temporary()
static let cacheController: PermanentCacheController = {
let tempPath = WMFRandomTemporaryPath()!
let randomURL = NSURL.fileURL(withPath: tempPath)
let temporaryCacheURL = randomURL.appendingPathComponent("Permanent Cache", isDirectory: true)
return PermanentCacheController.testController(with: temporaryCacheURL, dataStore: dataStore)
}()
static func pullDataFromFixtures(inBundle bundle: Bundle) {
guard let image = bundle.wmf_data(fromContentsOfFile:"960px-Flag_of_the_United_States.svg", ofType:"png"),
let html = bundle.wmf_data(fromContentsOfFile:"basic", ofType:"html"),
let css = bundle.wmf_data(fromContentsOfFile:"basic", ofType:"css"),
let cachedHTML = bundle.wmf_data(fromContentsOfFile:"basicCached", ofType:"html"),
let cachedCSS = bundle.wmf_data(fromContentsOfFile:"basicCached", ofType:"css"),
let cachedZhansHTML = bundle.wmf_data(fromContentsOfFile:"basicZhans", ofType:"html"),
let cachedZhCSS = bundle.wmf_data(fromContentsOfFile:"basicZh", ofType:"css"),
let imageZh640 = bundle.wmf_data(fromContentsOfFile:"640px-Flag_of_the_United_States_(Pantone).svg", ofType:"png") else {
assertionFailure("Error setting up fixtures.")
return
}
self.fixtureData = FixtureData(image: image, html: html, css: css, cachedHTML: cachedHTML, cachedCSS: cachedCSS, cachedZhansHTML: cachedZhansHTML, cachedZhCSS: cachedZhCSS, imageZh640: imageZh640)
}
static func stubCompleteMobileHTMLResponse(inBundle bundle: Bundle) {
guard let dogSummaryJSONData = bundle.wmf_data(fromContentsOfFile: "DogArticleSummary", ofType: "json"),
let dogCollageImageData = bundle.wmf_data(fromContentsOfFile:"CollageOfNineDogs", ofType:"jpg"),
let userGroupsData = bundle.wmf_data(fromContentsOfFile:"UserInfoGroups", ofType:"json"),
let mobileHTMLData = bundle.wmf_data(fromContentsOfFile:"DogMobileHTML", ofType:"html"),
let mobileBaseCSSData = bundle.wmf_data(fromContentsOfFile:"MobileBase", ofType:"css"),
let mobileSiteCSSData = bundle.wmf_data(fromContentsOfFile:"MobileSite", ofType:"css"),
let mobilePCSCSSData = bundle.wmf_data(fromContentsOfFile:"MobilePCS", ofType:"css"),
let mobilePCSJSData = bundle.wmf_data(fromContentsOfFile:"MobilePCS", ofType:"js"),
let i18PCSData = bundle.wmf_data(fromContentsOfFile:"PCSI18N", ofType: "json"),
let redPencilImageData = bundle.wmf_data(fromContentsOfFile:"RedPencilIcon", ofType:"png"),
let commonsLogoImageData = bundle.wmf_data(fromContentsOfFile:"59pxCommonsLogo.svg", ofType:"webp"),
let footerDog1ImageData = bundle.wmf_data(fromContentsOfFile:"64pxAussieBlacktri", ofType:"jpg"),
let footerDog2ImageData = bundle.wmf_data(fromContentsOfFile:"64pxOkapi2", ofType:"jpg"),
let wiktionaryLogoImageData = bundle.wmf_data(fromContentsOfFile:"54pxWiktionaryLogoV2.svg", ofType:"webp"),
let smallCommonsLogoImageData = bundle.wmf_data(fromContentsOfFile:"40pxCommonsLogo.svg", ofType:"webp"),
let wikiQuoteLogoImageData = bundle.wmf_data(fromContentsOfFile:"46pxWikiquoteLogo.svg", ofType:"webp"),
let wikiSpeciesLogoImageData = bundle.wmf_data(fromContentsOfFile:"46pxWikispeciesLogo.svg", ofType:"webp"),
let wikiSourceLogoImageData = bundle.wmf_data(fromContentsOfFile:"51pxWikisourceLogo.svg", ofType:"webp"),
let wikiBooksLogoImageData = bundle.wmf_data(fromContentsOfFile:"54pxWikibooksLogo.svg", ofType:"webp"),
let wikiNewsLogoImageData = bundle.wmf_data(fromContentsOfFile:"54pxWikinewsLogo.svg", ofType:"webp"),
let genericDogImageData = bundle.wmf_data(fromContentsOfFile:"640pxDogMorphologicalVariation", ofType:"png"),
let significantEventsData = bundle.wmf_data(fromContentsOfFile:"SignificantEventsDog", ofType: "json"),
let dailyMetricsData = bundle.wmf_data(fromContentsOfFile:"DogDailyMetrics", ofType: "json"),
let dailyMetricsRegex = try? NSRegularExpression(pattern: "https://wikimedia.org/api/rest_v1/metrics/edits/per-page/en.wikipedia.org/Dog/all-editor-types/daily/(.*?)/(.*?)", options: []),
let imageRegex = try? NSRegularExpression(pattern: "https://upload.wikimedia.org/wikipedia/commons/thumb.*", options: []) else {
assertionFailure("Error setting up fixtures.")
return
}
_ = stubRequest("GET", "https://en.wikipedia.org/api/rest_v1/page/summary/Dog" as NSString)
.andReturn(200)?
.withHeaders(["Content-Type": "application/json"])?
.withBody(dogSummaryJSONData as NSData)
_ = stubRequest("GET", "https://en.wikipedia.org/api/rest_v1/page/summary/Cat" as NSString)
.andReturn(200)?
.withHeaders(["Content-Type": "application/json"])?
.withBody(dogSummaryJSONData as NSData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Collage_of_Nine_Dogs.jpg/1280px-Collage_of_Nine_Dogs.jpg" as NSString)
.andReturnRawResponse(dogCollageImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/6/69/Dog_morphological_variation.png/640px-Dog_morphological_variation.png" as NSString)
.andReturnRawResponse(genericDogImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Llop.jpg/320px-Llop.jpg" as NSString)
.andReturnRawResponse(genericDogImageData)
_ = stubRequest("POST", "https://en.wikipedia.org/w/api.php" as NSString)
.withBody("action=query&format=json&meta=userinfo&uiprop=groups" as NSString)?
.andReturn(200)?
.withBody(userGroupsData as NSData)
_ = stubRequest("GET", "https://en.wikipedia.org/api/rest_v1/page/mobile-html/Dog" as NSString)
.andReturn(200)?
.withBody(mobileHTMLData as NSData)
_ = stubRequest("GET", "https://en.wikipedia.org/api/rest_v1/page/mobile-html/Cat" as NSString)
.andReturn(200)?
.withBody(mobileHTMLData as NSData)
_ = stubRequest("GET", "https://mobileapps-ios-experiments.wmflabs.org/en.wikipedia.org/v1/page/significant-events/Dog" as NSString)
.andReturn(200)?
.withBody(significantEventsData as NSData)
_ = stubRequest("GET", dailyMetricsRegex)
.andReturn(200)?
.withBody(dailyMetricsData as NSData)
_ = stubRequest("GET", "https://meta.wikimedia.org/api/rest_v1/data/css/mobile/base" as NSString)
.andReturn(200)?
.withBody(mobileBaseCSSData as NSData)
_ = stubRequest("GET", "https://en.wikipedia.org/api/rest_v1/data/css/mobile/site" as NSString)
.andReturn(200)?
.withBody(mobileSiteCSSData as NSData)
_ = stubRequest("GET", "https://meta.wikimedia.org/api/rest_v1/data/css/mobile/pcs" as NSString)
.andReturn(200)?
.withBody(mobilePCSCSSData as NSData)
_ = stubRequest("GET", "https://meta.wikimedia.org/api/rest_v1/data/javascript/mobile/pcs" as NSString)
.andReturn(200)?
.withBody(mobilePCSJSData as NSData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/7/74/Red_Pencil_Icon.png" as NSString)
.andReturnRawResponse(redPencilImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/en/thumb/4/4a/Commons-logo.svg/59px-Commons-logo.svg.png" as NSString)
.andReturnRawResponse(commonsLogoImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Aussie-blacktri.jpg/64px-Aussie-blacktri.jpg" as NSString)
.andReturnRawResponse(footerDog1ImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/Okapi2.jpg/64px-Okapi2.jpg" as NSString)
.andReturnRawResponse(footerDog2ImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/Okapi2.jpg/64px-Okapi2.jpg" as NSString)
.andReturnRawResponse(footerDog2ImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/en/thumb/0/06/Wiktionary-logo-v2.svg/54px-Wiktionary-logo-v2.svg.png" as NSString)
.andReturnRawResponse(wiktionaryLogoImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/en/thumb/4/4a/Commons-logo.svg/40px-Commons-logo.svg.png" as NSString)
.andReturnRawResponse(smallCommonsLogoImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Wikinews-logo.svg/54px-Wikinews-logo.svg.png" as NSString)
.andReturnRawResponse(wikiNewsLogoImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Wikiquote-logo.svg/46px-Wikiquote-logo.svg.png" as NSString)
.andReturnRawResponse(wikiQuoteLogoImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Wikisource-logo.svg/51px-Wikisource-logo.svg.png" as NSString)
.andReturnRawResponse(wikiSourceLogoImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Wikibooks-logo.svg/54px-Wikibooks-logo.svg.png" as NSString)
.andReturnRawResponse(wikiBooksLogoImageData)
_ = stubRequest("GET", "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/Wikispecies-logo.svg/46px-Wikispecies-logo.svg.png" as NSString)
.andReturnRawResponse(wikiSpeciesLogoImageData)
_ = stubRequest("GET", "https://en.wikipedia.org/api/rest_v1/data/i18n/pcs" as NSString)
.andReturn(200)?
.withBody(i18PCSData as NSData)
_ = stubRequest("GET", imageRegex)
.andReturnRawResponse(genericDogImageData)
}
static func writeCachedPiecesToCachingSystem() {
let moc = cacheController.managedObjectContext
let mobileHTMLURLString = "https://en.wikipedia.org/api/rest_v1/page/mobile-html/United_States"
let cssString = "https://en.wikipedia.org/api/rest_v1/data/css/mobile/site"
// save objects database
guard let cacheGroup = CacheDBWriterHelper.createCacheGroup(with: mobileHTMLURLString, in: moc),
let htmlCacheItem = CacheDBWriterHelper.createCacheItem(with: URL(string: mobileHTMLURLString)!, itemKey: mobileHTMLURLString, variant: nil, in: moc),
let imageCacheItem = CacheDBWriterHelper.createCacheItem(with: URL(string: "https://upload.wikimedia.org/wikipedia/en/thumb/a/a4/Flag_of_the_United_States.svg/960px-Flag_of_the_United_States.svg.png")!, itemKey: "upload.wikimedia.org__Flag_of_the_United_States.svg", variant: "960", in: moc),
let cssCacheItem = CacheDBWriterHelper.createCacheItem(with: URL(string: cssString)!, itemKey: cssString, variant: nil, in: moc) else {
XCTFail("Unable to create Cache DB objects")
return
}
cacheGroup.addToCacheItems(htmlCacheItem)
cacheGroup.addToCacheItems(imageCacheItem)
cacheGroup.addToCacheItems(cssCacheItem)
CacheDBWriterHelper.save(moc: moc) { (result) in
switch result {
case .success:
break
case .failure:
XCTFail("Failure saving Cache DB objects")
}
}
// set up file names and content
let fileNameGenerator = PermanentlyPersistableURLCache(moc: moc)
guard let htmlURL = URL(string: "https://en.wikipedia.org/api/rest_v1/page/mobile-html/United_States"),
let uniqueHTMLFileName = fileNameGenerator.uniqueFileNameForURL(htmlURL, type: .article),
let uniqueHTMLHeaderFileName = fileNameGenerator.uniqueHeaderFileNameForURL(htmlURL, type: .article) else {
XCTFail("Failure determining html file name")
return
}
guard let cssURL = URL(string: "https://en.wikipedia.org/api/rest_v1/data/css/mobile/site"),
let uniqueCSSFileName = fileNameGenerator.uniqueFileNameForURL(cssURL, type: .article),
let uniqueCSSHeaderFileName = fileNameGenerator.uniqueHeaderFileNameForURL(cssURL, type: .article) else {
XCTFail("Failure determining html file name")
return
}
guard let imageURL = URL(string: "https://upload.wikimedia.org/wikipedia/en/thumb/a/a4/Flag_of_the_United_States.svg/960px-Flag_of_the_United_States.svg.png"),
let uniqueImageFileName = fileNameGenerator.uniqueFileNameForURL(imageURL, type: .image),
let uniqueImageHeaderFileName = fileNameGenerator.uniqueHeaderFileNameForURL(imageURL, type: .image) else {
XCTFail("Failure determining image file name")
return
}
guard let fixtureData = self.fixtureData else {
XCTFail("Failure pulling data from Fixtures")
return
}
// save content in file system
CacheFileWriterHelper.saveData(data: fixtureData.cachedHTML, toNewFileWithKey: uniqueHTMLFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving html data")
}
}
CacheFileWriterHelper.saveData(data: fixtureData.image, toNewFileWithKey: uniqueImageFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving image data")
}
}
CacheFileWriterHelper.saveData(data: fixtureData.cachedCSS, toNewFileWithKey: uniqueCSSFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving css data")
}
}
// save headers in file system
let htmlHeaders: [String: String] = ["Content-Type": "text/html; charset=utf-8;"]
CacheFileWriterHelper.saveResponseHeader(headerFields: htmlHeaders, toNewFileName: uniqueHTMLHeaderFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving html header")
}
}
let imageHeaders: [String: String] = ["Content-Type": "image/jpeg",
"Content-Length": String((fixtureData.image as NSData).length)]
CacheFileWriterHelper.saveResponseHeader(headerFields: imageHeaders, toNewFileName: uniqueImageHeaderFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving image header")
}
}
let cssHeaders: [String: String] = ["Content-Type": "text/css; charset=utf-8;",
"Content-Length": String((fixtureData.cachedCSS as NSData).length)]
CacheFileWriterHelper.saveResponseHeader(headerFields: cssHeaders, toNewFileName: uniqueCSSHeaderFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving css header")
}
}
}
static func writeVariantPiecesToCachingSystem() {
let moc = cacheController.managedObjectContext
let mobileHTMLURLString = "https://zh.wikipedia.org/api/rest_v1/page/mobile-html/%E7%BE%8E%E5%9B%BD"
let cssString = "https://zh.wikipedia.org/api/rest_v1/data/css/mobile/site"
let imageURLString = "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Flag_of_the_United_States_%28Pantone%29.svg/640px-Flag_of_the_United_States_%28Pantone%29.svg.png"
// save objects database
guard let cacheGroup = CacheDBWriterHelper.createCacheGroup(with: mobileHTMLURLString, in: moc),
let htmlCacheItem = CacheDBWriterHelper.createCacheItem(with: URL(string: mobileHTMLURLString)!, itemKey: mobileHTMLURLString, variant: "zh-hans", in: moc),
let imageCacheItem = CacheDBWriterHelper.createCacheItem(with: URL(string: imageURLString)!, itemKey: "upload.wikimedia.org__Flag_of_the_United_States_%28Pantone%29.svg", variant: "640", in: moc),
let cssCacheItem = CacheDBWriterHelper.createCacheItem(with: URL(string: cssString)!, itemKey: cssString, variant: nil, in: moc) else {
XCTFail("Unable to create Cache DB objects")
return
}
imageCacheItem.isDownloaded = true
cacheGroup.addToCacheItems(htmlCacheItem)
cacheGroup.addToCacheItems(imageCacheItem)
cacheGroup.addToCacheItems(cssCacheItem)
CacheDBWriterHelper.save(moc: moc) { (result) in
switch result {
case .success:
break
case .failure:
XCTFail("Failure saving Cache DB objects")
}
}
// set up file names and content
let fileNameGenerator = PermanentlyPersistableURLCache(moc: moc)
guard let htmlURL = URL(string: mobileHTMLURLString),
let uniqueHTMLFileName = fileNameGenerator.uniqueFileNameForURL(htmlURL, type: .article),
let uniqueHTMLHeaderFileName = fileNameGenerator.uniqueHeaderFileNameForURL(htmlURL, type: .article) else {
XCTFail("Failure determining html file name")
return
}
guard let cssURL = URL(string: cssString),
let uniqueCSSFileName = fileNameGenerator.uniqueFileNameForURL(cssURL, type: .article),
let uniqueCSSHeaderFileName = fileNameGenerator.uniqueHeaderFileNameForURL(cssURL, type: .article) else {
XCTFail("Failure determining html file name")
return
}
guard let imageURL = URL(string: imageURLString),
let uniqueImageFileName = fileNameGenerator.uniqueFileNameForURL(imageURL, type: .image),
let uniqueImageHeaderFileName = fileNameGenerator.uniqueHeaderFileNameForURL(imageURL, type: .image) else {
XCTFail("Failure determining image file name")
return
}
guard let fixtureData = self.fixtureData else {
XCTFail("Failure pulling data from Fixtures")
return
}
// save content in file system
CacheFileWriterHelper.saveData(data: fixtureData.cachedZhansHTML, toNewFileWithKey: uniqueHTMLFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving html data")
}
}
CacheFileWriterHelper.saveData(data: fixtureData.imageZh640, toNewFileWithKey: uniqueImageFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving image data")
}
}
CacheFileWriterHelper.saveData(data: fixtureData.cachedZhCSS, toNewFileWithKey: uniqueCSSFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving css data")
}
}
// save headers in file system
let htmlHeaders: [String: String] = ["Content-Type": "text/html; charset=utf-8;",
"Vary": "Accept-Language, Accept-Encoding"]
CacheFileWriterHelper.saveResponseHeader(headerFields: htmlHeaders, toNewFileName: uniqueHTMLHeaderFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving html header")
}
}
let imageHeaders: [String: String] = ["Content-Type": "image/jpeg",
"Content-Length": String((fixtureData.imageZh640 as NSData).length)]
CacheFileWriterHelper.saveResponseHeader(headerFields: imageHeaders, toNewFileName: uniqueImageHeaderFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving image header")
}
}
let cssHeaders: [String: String] = ["Content-Type": "text/css; charset=utf-8;",
"Content-Length": String((fixtureData.cachedZhCSS as NSData).length)]
CacheFileWriterHelper.saveResponseHeader(headerFields: cssHeaders, toNewFileName: uniqueCSSHeaderFileName) { (result) in
switch result {
case .success, .exists:
break
case .failure:
XCTFail("Failure saving css header")
}
}
}
}