// // PackageJSONTests.swift // KeymanEngineTests // // Created by Joshua Horton on 6/1/20. // Copyright © 2020 SIL International. All rights reserved. // import XCTest import Reachability @testable import KeymanEngine class KMPJSONTests: XCTestCase { private func loadObjectFromJSON(at file: URL) throws -> Type { let fileContents: String = try String(contentsOf: file, encoding: .utf8).replacingOccurrences(of: "\r", with: "") let jsonData = fileContents.data(using: .utf8)! let decoder = JSONDecoder() return try decoder.decode(Type.self, from: jsonData) } func testLanguageDecoding() throws { let lang_en: KMPLanguage = try loadObjectFromJSON(at: TestUtils.PackageJSON.language_en) XCTAssertEqual(lang_en.name, "English") XCTAssertEqual(lang_en.languageId, "en") let lang_km: KMPLanguage = try loadObjectFromJSON(at: TestUtils.PackageJSON.language_km) XCTAssertEqual(lang_km.name, "Central Khmer (Khmer, Cambodia)") XCTAssertEqual(lang_km.languageId, "km") let lang_str_latn: KMPLanguage = try loadObjectFromJSON(at: TestUtils.PackageJSON.language_str_latn) XCTAssertEqual(lang_str_latn.name, "SENĆOŦEN") // Is upper-cased in the .json, but should be lower-cased on deserialization. // Our language-tag matching is case-insensitive. XCTAssertEqual(lang_str_latn.languageId, "str-latn") } // A helper method, since these are useful in two separate tests. func keyboard_khmer_angkor_assertions(_ khmer_angkor: KMPKeyboard) { XCTAssertEqual(khmer_angkor.languages.count, 1) XCTAssertEqual(khmer_angkor.languages[0].name, "Central Khmer (Khmer, Cambodia)") XCTAssertEqual(khmer_angkor.languages[0].languageId, "km") XCTAssertEqual(khmer_angkor.name, "Khmer Angkor") XCTAssertEqual(khmer_angkor.keyboardId, "khmer_angkor") XCTAssertEqual(khmer_angkor.version, "1.0.6") XCTAssertEqual(khmer_angkor.font, "Mondulkiri-R.ttf") XCTAssertEqual(khmer_angkor.osk, "Mondulkiri-R.ttf") XCTAssertEqual(khmer_angkor.isRTL, false) } func testKeyboardDecoding() throws { let khmer_angkor: KMPKeyboard = try loadObjectFromJSON(at: TestUtils.PackageJSON.keyboard_khmer_angkor) keyboard_khmer_angkor_assertions(khmer_angkor) } // A helper method, since these are useful in two separate tests. func lexical_model_nrc_en_mtnt_assertions(_ nrc_en_mtnt: KMPLexicalModel, version: String? = nil) { XCTAssertEqual(nrc_en_mtnt.name, "English dictionary (MTNT)") XCTAssertEqual(nrc_en_mtnt.lexicalModelId, "nrc.en.mtnt") // Our example case does not define either of these two values. // One (isRTL), we assume a default value for. XCTAssertEqual(nrc_en_mtnt.version, version) XCTAssertEqual(nrc_en_mtnt.isRTL, false) XCTAssertEqual(nrc_en_mtnt.languages.count, 3) XCTAssertEqual(nrc_en_mtnt.languages[0].languageId, "en") XCTAssertEqual(nrc_en_mtnt.languages[1].languageId, "en-us") XCTAssertEqual(nrc_en_mtnt.languages[2].languageId, "en-ca") } func testLexicalModelDecoding() throws { let nrc_en_mtnt: KMPLexicalModel = try loadObjectFromJSON(at: TestUtils.PackageJSON.model_nrc_en_mtnt) lexical_model_nrc_en_mtnt_assertions(nrc_en_mtnt) } func kmp_info_khmer_angkor_assertions(_ khmer_angkor: KMPMetadata) { XCTAssertTrue(khmer_angkor.isValid) XCTAssertEqual(khmer_angkor.packageType, KMPMetadata.PackageType.Keyboard) XCTAssertNotNil(khmer_angkor.keyboards) XCTAssertEqual(khmer_angkor.keyboards!.count, 1) keyboard_khmer_angkor_assertions(khmer_angkor.keyboards![0]) XCTAssertNil(khmer_angkor.lexicalModels) XCTAssertNotNil(khmer_angkor.files) XCTAssertEqual(khmer_angkor.files!.count, 16) XCTAssertNotNil(khmer_angkor.info) XCTAssertEqual(khmer_angkor.info!.version!.description, "1.0.6") XCTAssertNil(khmer_angkor.info!.version!.url) XCTAssertEqual(khmer_angkor.info!.author!.description, "Makara Sok") XCTAssertEqual(khmer_angkor.info!.author!.url, "mailto:makara@keyman.com") XCTAssertEqual(khmer_angkor.system.fileVersion, "7.0") XCTAssertNil(khmer_angkor.options.graphicFile) XCTAssertNil(khmer_angkor.options.readmeFile) XCTAssertFalse(khmer_angkor.isAutogeneratedWrapper) } func testKeyboardPackageInfoDecoding() throws { let khmer_angkor: KMPMetadata = try loadObjectFromJSON(at: TestUtils.PackageJSON.kmp_json_khmer_angkor) kmp_info_khmer_angkor_assertions(khmer_angkor) } func kmp_info_nrc_en_mtnt_assertions(_ nrc_en_mtnt: KMPMetadata, version: String? = nil) { XCTAssertTrue(nrc_en_mtnt.isValid) XCTAssertEqual(nrc_en_mtnt.packageType, KMPMetadata.PackageType.LexicalModel) XCTAssertNotNil(nrc_en_mtnt.lexicalModels) XCTAssertEqual(nrc_en_mtnt.lexicalModels!.count, 1) lexical_model_nrc_en_mtnt_assertions(nrc_en_mtnt.lexicalModels![0], version: version) XCTAssertNil(nrc_en_mtnt.keyboards) XCTAssertNotNil(nrc_en_mtnt.files) XCTAssertEqual(nrc_en_mtnt.files!.count, 1) XCTAssertNotNil(nrc_en_mtnt.info) XCTAssertEqual(nrc_en_mtnt.info!.version!.description, "0.1.4") XCTAssertNil(nrc_en_mtnt.info!.version!.url) XCTAssertEqual(nrc_en_mtnt.info!.author!.description, "Eddie Antonio Santos") XCTAssertEqual(nrc_en_mtnt.info!.author!.url, "mailto:easantos@ualberta.ca") XCTAssertEqual(nrc_en_mtnt.system.fileVersion, "12.0") XCTAssertNil(nrc_en_mtnt.options.graphicFile) XCTAssertNil(nrc_en_mtnt.options.readmeFile) XCTAssertFalse(nrc_en_mtnt.isAutogeneratedWrapper) } func testLexicalModelPackageInfoDecoding() throws { let nrc_en_mtnt: KMPMetadata = try loadObjectFromJSON(at: TestUtils.PackageJSON.kmp_json_nrc_en_mtnt) kmp_info_nrc_en_mtnt_assertions(nrc_en_mtnt) } func testKMPKeyboardFrom() throws { guard let constructedMetadata = KMPKeyboard(from: TestUtils.Keyboards.khmer_angkor) else { XCTFail("Could not construct KMPKeyboard from InstallableKeyboard") return } // The original version extracted from a KMP. let kmpMetadata: KMPKeyboard = try loadObjectFromJSON(at: TestUtils.PackageJSON.keyboard_khmer_angkor)! XCTAssertEqual(constructedMetadata.id, kmpMetadata.id) XCTAssertEqual(constructedMetadata.name, kmpMetadata.name) XCTAssertEqual(constructedMetadata.version, kmpMetadata.version) // The pre-extracted version currently does not have fonts set, so the following two lines will fail. // XCTAssertEqual(constructedMetadata.font, kmpMetadata.font) // XCTAssertEqual(constructedMetadata.osk, kmpMetadata.osk) XCTAssertEqual(constructedMetadata.isRTL, kmpMetadata.isRTL) // Language assertions are not as easy. XCTAssertTrue(kmpMetadata.languages.contains(where: { language in // There will only be a single language in our 'constructed' version, since it's built // from a single LanguageResource. let constructedLanguage = constructedMetadata.languages[0] return constructedLanguage.languageId == language.languageId && constructedLanguage.name == language.name })) } func testKMPLexicalModelFrom() throws { guard let constructedMetadata = KMPLexicalModel(from: TestUtils.LexicalModels.mtnt) else { XCTFail("Could not construct KMPLexicalModel from InstallableLexicalModel") return } // The original version extracted from a KMP. let kmpMetadata: KMPLexicalModel = try loadObjectFromJSON(at: TestUtils.PackageJSON.model_nrc_en_mtnt)! XCTAssertEqual(constructedMetadata.id, kmpMetadata.id) XCTAssertEqual(constructedMetadata.name, kmpMetadata.name) // The following line will fail because kmpMetadata.version was only set on the package level, // not directly within the KMPLexicalModel definition. //XCTAssertEqual(constructedMetadata.version, kmpMetadata.version) XCTAssertEqual(constructedMetadata.isRTL, kmpMetadata.isRTL) // Language assertions are not as easy. XCTAssertTrue(kmpMetadata.languages.contains(where: { language in // There will only be a single language in our 'constructed' version, since it's built // from a single LanguageResource. We can't reconstruct the original language name // for lexical models at this time. let constructedLanguage = constructedMetadata.languages[0] return constructedLanguage.languageId == language.languageId })) } func testKMPResourceMatches() throws { let kmp_khmer_angkor: KMPKeyboard = try loadObjectFromJSON(at: TestUtils.PackageJSON.keyboard_khmer_angkor)! XCTAssertTrue(kmp_khmer_angkor.hasMatchingMetadata(for: TestUtils.Keyboards.khmer_angkor)) let kmp_mtnt: KMPLexicalModel = try loadObjectFromJSON(at: TestUtils.PackageJSON.model_nrc_en_mtnt)! kmp_mtnt.setNilVersion(to: "0.1.4") XCTAssertTrue(kmp_mtnt.hasMatchingMetadata(for: TestUtils.LexicalModels.mtnt)) // Three models are listed; we choose the second variant since it uses a different language // code than specified under TestUtils.LexicalModels.mtnt. kmp_mtnt.packageId = "nrc.en.mtnt" // We didn't load the full package, so the id's unset. let constructed_mtnt: KMPLexicalModel = KMPLexicalModel(from: kmp_mtnt.installableResources[1])! XCTAssertFalse(constructed_mtnt.hasMatchingMetadata(for: TestUtils.LexicalModels.mtnt)) XCTAssertTrue(constructed_mtnt.hasMatchingMetadata(for: TestUtils.LexicalModels.mtnt, ignoreLanguage: true)) } func testKMPMetadataFromResource() throws { guard let skipTest = ProcessInfo.processInfo.environment["SKIP_UNSTABLE_TESTS"] else { XCTFail("Test environment not configured properly") return } try XCTSkipIf(Bool.init(skipTest) ?? false) // https://assertible.com/json-schema-validation#api // - Note: they "do not make any guarantees about the uptime and availability // of the Free JSON Schema Validation API." let API_ENDPOINT = URL(string: "https://assertible.com/json")! // So, first let's make sure that the API endpoint actually IS available. // If not, skip this test. let reachable = try Reachability(hostname: "https://assertible.com/json") try XCTSkipIf(reachable.connection == .unavailable, "API endpoint for test not available") // - build the arguments for the API call let constructedMetadata_khmer_angkor = KMPMetadata(from: TestUtils.Keyboards.khmer_angkor) XCTAssertTrue(constructedMetadata_khmer_angkor.isAutogeneratedWrapper) let coder = JSONEncoder() let codedJSON = try coder.encode(constructedMetadata_khmer_angkor) let schemaURL = URL(string: "\(KeymanHosts.API_KEYMAN_COM)/schemas/package.json")! let schemaData = try Data(contentsOf: schemaURL) // - arguments ready, time to build the POST-based request // This section brought to you by: https://stackoverflow.com/questions/41997641/how-to-make-nsurlsession-post-request-in-swift var request = URLRequest(url: API_ENDPOINT) request.httpMethod = "POST" request.setValue("Application/json", forHTTPHeaderField: "Content-Type") let parameterDictionary = [ "schema": try JSONSerialization.jsonObject(with: schemaData, options: []), "json": try JSONSerialization.jsonObject(with: codedJSON, options: []) ] request.httpBody = try? JSONSerialization.data(withJSONObject: parameterDictionary, options: []) // - and NOW to finally make the API call for the test. let expectation = XCTestExpectation(description: "API call return and analysis expected") let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { XCTFail("Could not complete API call due to error: \(String(describing: error))") expectation.fulfill() return } do { if let results: [String: Any] = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String : Any] { if let valid = results["valid"] as? Bool, let errors = results["errors"] as? [String] { XCTAssert(valid, "Errors returned from schema validation API call: \(String(describing: errors))") } else { XCTFail("Unexpected format in return object from API") } } else { XCTFail("Unexpected format in return object from API") } } catch { XCTFail("Error occurred processing returned data - may not be JSON") } expectation.fulfill() } task.resume() wait(for: [expectation], timeout: 10) } }