Separate parser and helpers from Password class for better testability

This commit is contained in:
Danny Moesch 2018-11-11 18:09:52 +01:00 committed by Bob Sun
parent 2abbceb2e9
commit 7c12263458
17 changed files with 913 additions and 537 deletions

View file

@ -9,7 +9,16 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
18F19A67B0C07F13C17169E0 /* Pods_pass.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5620D17DF5E86B61761D0E /* Pods_pass.framework */; }; 18F19A67B0C07F13C17169E0 /* Pods_pass.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A5620D17DF5E86B61761D0E /* Pods_pass.framework */; };
23B82F0228254275DBA609E7 /* Pods_passExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B975797E0F0B7476CADD6A7D /* Pods_passExtension.framework */; }; 23B82F0228254275DBA609E7 /* Pods_passExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B975797E0F0B7476CADD6A7D /* Pods_passExtension.framework */; };
30B04860209A5141001013CA /* PasswordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0485F209A5141001013CA /* PasswordTests.swift */; }; 301F6463216162550071A4CE /* AdditionField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F6462216162550071A4CE /* AdditionField.swift */; };
301F6466216164830071A4CE /* PasswordHelpersTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F6465216164830071A4CE /* PasswordHelpersTest.swift */; };
301F6468216165290071A4CE /* ConstantsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F6467216165290071A4CE /* ConstantsTest.swift */; };
301F646A216166000071A4CE /* StringExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F6469216166000071A4CE /* StringExtensionTest.swift */; };
301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301F646C216166AA0071A4CE /* AdditionFieldTest.swift */; };
302E85612125ECC70031BA64 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302E85602125ECC70031BA64 /* Parser.swift */; };
302E85632125EE550031BA64 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 302E85622125EE550031BA64 /* Constants.swift */; };
30AAC05321989DCE00F656CE /* PasswordHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30AAC05221989DCE00F656CE /* PasswordHelpers.swift */; };
30B04860209A5141001013CA /* PasswordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0485F209A5141001013CA /* PasswordTest.swift */; };
30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FD2F77214D9E0E005E0A92 /* ParserTest.swift */; };
61326CDA7A73757FB68DCB04 /* Pods_passKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAB3F5541E51ADC8C6B56642 /* Pods_passKit.framework */; }; 61326CDA7A73757FB68DCB04 /* Pods_passKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DAB3F5541E51ADC8C6B56642 /* Pods_passKit.framework */; };
A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; }; A20691F41F2A3D0E0096483D /* SecurePasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20691F31F2A3D0E0096483D /* SecurePasteboard.swift */; };
A2168A7F1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2168A7E1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift */; }; A2168A7F1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2168A7E1EFD40D5005EA873 /* OnePasswordExtensionConstants.swift */; };
@ -175,7 +184,16 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
30B0485F209A5141001013CA /* PasswordTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTests.swift; sourceTree = "<group>"; }; 301F6462216162550071A4CE /* AdditionField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionField.swift; sourceTree = "<group>"; };
301F6465216164830071A4CE /* PasswordHelpersTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordHelpersTest.swift; sourceTree = "<group>"; };
301F6467216165290071A4CE /* ConstantsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsTest.swift; sourceTree = "<group>"; };
301F6469216166000071A4CE /* StringExtensionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensionTest.swift; sourceTree = "<group>"; };
301F646C216166AA0071A4CE /* AdditionFieldTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionFieldTest.swift; sourceTree = "<group>"; };
302E85602125ECC70031BA64 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
302E85622125EE550031BA64 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
30AAC05221989DCE00F656CE /* PasswordHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PasswordHelpers.swift; path = Helpers/PasswordHelpers.swift; sourceTree = "<group>"; };
30B0485F209A5141001013CA /* PasswordTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTest.swift; sourceTree = "<group>"; };
30FD2F77214D9E0E005E0A92 /* ParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTest.swift; sourceTree = "<group>"; };
31C3033E8868D05B2C55C8B1 /* Pods-passExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-passExtension/Pods-passExtension.debug.xcconfig"; sourceTree = "<group>"; }; 31C3033E8868D05B2C55C8B1 /* Pods-passExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-passExtension/Pods-passExtension.debug.xcconfig"; sourceTree = "<group>"; };
3A5620D17DF5E86B61761D0E /* Pods_pass.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_pass.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3A5620D17DF5E86B61761D0E /* Pods_pass.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_pass.framework; sourceTree = BUILT_PRODUCTS_DIR; };
666769E0B255666D02945C15 /* Pods-passKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-passKitTests/Pods-passKitTests.release.xcconfig"; sourceTree = "<group>"; }; 666769E0B255666D02945C15 /* Pods-passKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-passKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-passKitTests/Pods-passKitTests.release.xcconfig"; sourceTree = "<group>"; };
@ -361,6 +379,43 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
301F6464216164670071A4CE /* Helpers */ = {
isa = PBXGroup;
children = (
301F6465216164830071A4CE /* PasswordHelpersTest.swift */,
301F6469216166000071A4CE /* StringExtensionTest.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
30C015A3214ECF2B005BB6DF /* Parser */ = {
isa = PBXGroup;
children = (
301F6462216162550071A4CE /* AdditionField.swift */,
302E85622125EE550031BA64 /* Constants.swift */,
302E85602125ECC70031BA64 /* Parser.swift */,
);
path = Parser;
sourceTree = "<group>";
};
30C015A6214ED32A005BB6DF /* Parser */ = {
isa = PBXGroup;
children = (
301F646C216166AA0071A4CE /* AdditionFieldTest.swift */,
301F6467216165290071A4CE /* ConstantsTest.swift */,
30FD2F77214D9E0E005E0A92 /* ParserTest.swift */,
);
path = Parser;
sourceTree = "<group>";
};
30C015A7214ED378005BB6DF /* Models */ = {
isa = PBXGroup;
children = (
30B0485F209A5141001013CA /* PasswordTest.swift */,
);
path = Models;
sourceTree = "<group>";
};
A2168A801EFD431A005EA873 /* Controllers */ = { A2168A801EFD431A005EA873 /* Controllers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -413,6 +468,7 @@
A2F4E20F1EED7F0A0011986E /* Helpers */, A2F4E20F1EED7F0A0011986E /* Helpers */,
A260757B1EEC6F34005DB03E /* Info.plist */, A260757B1EEC6F34005DB03E /* Info.plist */,
A2F4E20E1EED7F040011986E /* Models */, A2F4E20E1EED7F040011986E /* Models */,
30C015A3214ECF2B005BB6DF /* Parser */,
A26075A51EEC7125005DB03E /* pass.xcdatamodeld */, A26075A51EEC7125005DB03E /* pass.xcdatamodeld */,
A260757A1EEC6F34005DB03E /* passKit.h */, A260757A1EEC6F34005DB03E /* passKit.h */,
); );
@ -422,9 +478,11 @@
A26075861EEC6F34005DB03E /* passKitTests */ = { A26075861EEC6F34005DB03E /* passKitTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
301F6464216164670071A4CE /* Helpers */,
30C015A7214ED378005BB6DF /* Models */,
30C015A6214ED32A005BB6DF /* Parser */,
A26075871EEC6F34005DB03E /* passKitTests.swift */, A26075871EEC6F34005DB03E /* passKitTests.swift */,
A26075891EEC6F34005DB03E /* Info.plist */, A26075891EEC6F34005DB03E /* Info.plist */,
30B0485F209A5141001013CA /* PasswordTests.swift */,
); );
path = passKitTests; path = passKitTests;
sourceTree = "<group>"; sourceTree = "<group>";
@ -455,8 +513,8 @@
A2F4E20E1EED7F040011986E /* Models */ = { A2F4E20E1EED7F040011986E /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A2C532BA201E5A9600DB9F53 /* PasscodeLock.swift */,
A2F4E2101EED800F0011986E /* GitCredential.swift */, A2F4E2101EED800F0011986E /* GitCredential.swift */,
A2C532BA201E5A9600DB9F53 /* PasscodeLock.swift */,
A2F4E2111EED800F0011986E /* Password.swift */, A2F4E2111EED800F0011986E /* Password.swift */,
A2F4E2121EED800F0011986E /* PasswordEntity.swift */, A2F4E2121EED800F0011986E /* PasswordEntity.swift */,
A2F4E2131EED800F0011986E /* PasswordStore.swift */, A2F4E2131EED800F0011986E /* PasswordStore.swift */,
@ -469,13 +527,14 @@
children = ( children = (
A2F4E2181EED80160011986E /* AppError.swift */, A2F4E2181EED80160011986E /* AppError.swift */,
A2F4E2191EED80160011986E /* DefaultsKeys.swift */, A2F4E2191EED80160011986E /* DefaultsKeys.swift */,
A239F5202157B75E00576CBF /* FileManagerExtension.swift */,
A2F4E21A1EED80160011986E /* Globals.swift */, A2F4E21A1EED80160011986E /* Globals.swift */,
A2F4E21B1EED80160011986E /* NotificationNames.swift */, A2F4E21B1EED80160011986E /* NotificationNames.swift */,
A2F4E21D1EED80160011986E /* Utils.swift */, 30AAC05221989DCE00F656CE /* PasswordHelpers.swift */,
A239F51E2157B72700576CBF /* StringExtension.swift */,
A2F4E21C1EED80160011986E /* UITextFieldExtension.swift */, A2F4E21C1EED80160011986E /* UITextFieldExtension.swift */,
A2BEC1BA207D2EFE00F3051C /* UIViewExtension.swift */, A2BEC1BA207D2EFE00F3051C /* UIViewExtension.swift */,
A239F51E2157B72700576CBF /* StringExtension.swift */, A2F4E21D1EED80160011986E /* Utils.swift */,
A239F5202157B75E00576CBF /* FileManagerExtension.swift */,
); );
name = Helpers; name = Helpers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -577,15 +636,15 @@
DC917BCA1E2E8231000FDF54 = { DC917BCA1E2E8231000FDF54 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DC917BD51E2E8231000FDF54 /* pass */,
A26075791EEC6F34005DB03E /* passKit */,
A26700251EEC466A00176B8A /* passExtension */,
A239F5972158C08C00576CBF /* passAutoFillExtension */,
DC13B14F1E8640810097803F /* passTests */,
A26075861EEC6F34005DB03E /* passKitTests */,
DC917BD41E2E8231000FDF54 /* Products */,
DC917BED1E2F38C4000FDF54 /* Frameworks */, DC917BED1E2F38C4000FDF54 /* Frameworks */,
DC917BD51E2E8231000FDF54 /* pass */,
A239F5972158C08C00576CBF /* passAutoFillExtension */,
A26700251EEC466A00176B8A /* passExtension */,
A26075791EEC6F34005DB03E /* passKit */,
A26075861EEC6F34005DB03E /* passKitTests */,
DC13B14F1E8640810097803F /* passTests */,
A51B01737D08DB47BB58F85A /* Pods */, A51B01737D08DB47BB58F85A /* Pods */,
DC917BD41E2E8231000FDF54 /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -1088,6 +1147,8 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A2BEC1BB207D2EFE00F3051C /* UIViewExtension.swift in Sources */, A2BEC1BB207D2EFE00F3051C /* UIViewExtension.swift in Sources */,
302E85632125EE550031BA64 /* Constants.swift in Sources */,
301F6463216162550071A4CE /* AdditionField.swift in Sources */,
A2C532BB201E5A9600DB9F53 /* PasscodeLock.swift in Sources */, A2C532BB201E5A9600DB9F53 /* PasscodeLock.swift in Sources */,
A2F4E2151EED800F0011986E /* Password.swift in Sources */, A2F4E2151EED800F0011986E /* Password.swift in Sources */,
A26075AD1EEC7125005DB03E /* pass.xcdatamodeld in Sources */, A26075AD1EEC7125005DB03E /* pass.xcdatamodeld in Sources */,
@ -1095,10 +1156,12 @@
A239F5212157B75E00576CBF /* FileManagerExtension.swift in Sources */, A239F5212157B75E00576CBF /* FileManagerExtension.swift in Sources */,
A2F4E21E1EED80160011986E /* AppError.swift in Sources */, A2F4E21E1EED80160011986E /* AppError.swift in Sources */,
A2F4E2171EED800F0011986E /* PasswordStore.swift in Sources */, A2F4E2171EED800F0011986E /* PasswordStore.swift in Sources */,
302E85612125ECC70031BA64 /* Parser.swift in Sources */,
A2F4E2211EED80160011986E /* NotificationNames.swift in Sources */, A2F4E2211EED80160011986E /* NotificationNames.swift in Sources */,
A2F4E2221EED80160011986E /* UITextFieldExtension.swift in Sources */, A2F4E2221EED80160011986E /* UITextFieldExtension.swift in Sources */,
A2C532BF201E5AA100DB9F53 /* PasscodeLockPresenter.swift in Sources */, A2C532BF201E5AA100DB9F53 /* PasscodeLockPresenter.swift in Sources */,
A2C532BE201E5AA100DB9F53 /* PasscodeLockViewController.swift in Sources */, A2C532BE201E5AA100DB9F53 /* PasscodeLockViewController.swift in Sources */,
30AAC05321989DCE00F656CE /* PasswordHelpers.swift in Sources */,
A2F4E2201EED80160011986E /* Globals.swift in Sources */, A2F4E2201EED80160011986E /* Globals.swift in Sources */,
A2F4E2231EED80160011986E /* Utils.swift in Sources */, A2F4E2231EED80160011986E /* Utils.swift in Sources */,
A2F4E21F1EED80160011986E /* DefaultsKeys.swift in Sources */, A2F4E21F1EED80160011986E /* DefaultsKeys.swift in Sources */,
@ -1111,7 +1174,12 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
30B04860209A5141001013CA /* PasswordTests.swift in Sources */, 301F646A216166000071A4CE /* StringExtensionTest.swift in Sources */,
301F6466216164830071A4CE /* PasswordHelpersTest.swift in Sources */,
301F646D216166AA0071A4CE /* AdditionFieldTest.swift in Sources */,
30FD2F78214D9E0E005E0A92 /* ParserTest.swift in Sources */,
30B04860209A5141001013CA /* PasswordTest.swift in Sources */,
301F6468216165290071A4CE /* ConstantsTest.swift in Sources */,
A26075881EEC6F34005DB03E /* passKitTests.swift in Sources */, A26075881EEC6F34005DB03E /* passKitTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View file

@ -14,7 +14,7 @@ class EditPasswordTableViewController: PasswordEditorTableViewController {
tableData = [ tableData = [
[[.type: PasswordEditorCellType.nameCell, .title: "name", .content: password!.namePath]], [[.type: PasswordEditorCellType.nameCell, .title: "name", .content: password!.namePath]],
[[.type: PasswordEditorCellType.fillPasswordCell, .title: "password", .content: password!.password]], [[.type: PasswordEditorCellType.fillPasswordCell, .title: "password", .content: password!.password]],
[[.type: PasswordEditorCellType.additionsCell, .title: "additions", .content: password!.getAdditionsPlainText()]], [[.type: PasswordEditorCellType.additionsCell, .title: "additions", .content: password!.additionsPlainText]],
[[.type: PasswordEditorCellType.scanQRCodeCell], [[.type: PasswordEditorCellType.scanQRCodeCell],
[.type: PasswordEditorCellType.deletePasswordCell]] [.type: PasswordEditorCellType.deletePasswordCell]]
] ]

View file

@ -174,7 +174,7 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
} }
@objc func tapHideOTPSwitchDetailButton(_ sender: Any?) { @objc func tapHideOTPSwitchDetailButton(_ sender: Any?) {
let keywordsString = Password.OTP_KEYWORDS.joined(separator: ",") let keywordsString = Constants.OTP_KEYWORDS.joined(separator: ",")
let alertMessage = "Turn on this switch to hide the fields related to one time passwords (i.e., \(keywordsString))." let alertMessage = "Turn on this switch to hide the fields related to one time passwords (i.e., \(keywordsString))."
let alertTitle = "Hide One Time Password Fields" let alertTitle = "Hide One Time Password Fields"
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil) Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)

View file

@ -185,8 +185,7 @@ class PasswordEditorTableViewController: UITableViewController, FillPasswordTabl
// generate password, copy to pasteboard, and set the cell // generate password, copy to pasteboard, and set the cell
// check whether the current password looks like an OTP field // check whether the current password looks like an OTP field
func generateAndCopyPassword() { func generateAndCopyPassword() {
if let currentPassword = fillPasswordCell?.getContent(), if let currentPassword = fillPasswordCell?.getContent(), Constants.isOtpRelated(line: currentPassword) {
Password.LooksLikeOTP(line: currentPassword) {
let alert = UIAlertController(title: "Overwrite?", message: "Overwrite the one-time password configuration?", preferredStyle: UIAlertControllerStyle.alert) let alert = UIAlertController(title: "Overwrite?", message: "Overwrite the one-time password configuration?", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Yes", style: UIAlertActionStyle.destructive, handler: {_ in alert.addAction(UIAlertAction(title: "Yes", style: UIAlertActionStyle.destructive, handler: {_ in
self.generateAndCopyPasswordNoOtpCheck() self.generateAndCopyPasswordNoOtpCheck()

View file

@ -0,0 +1,30 @@
//
// PasswordHelpers.swift
// passKit
//
// Created by Danny Moesch on 17.08.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
import OneTimePassword
public enum OtpType {
case totp, hotp, none
init(token: Token?) {
switch token?.generator.factor {
case .some(.counter):
self = .hotp
case .some(.timer):
self = .totp
default:
self = .none
}
}
}
enum PasswordChange: Int {
case path = 0x01
case content = 0x02
case none = 0x00
}

View file

@ -16,3 +16,9 @@ public extension String {
return addingPercentEncoding(withAllowedCharacters: allowed) return addingPercentEncoding(withAllowedCharacters: allowed)
} }
} }
extension String {
static func | (left: String, right: String) -> String {
return right.isEmpty ? left : left + "\n" + right
}
}

View file

@ -6,163 +6,99 @@
// Copyright © 2017 Bob Sun. All rights reserved. // Copyright © 2017 Bob Sun. All rights reserved.
// //
import Foundation
import SwiftyUserDefaults import SwiftyUserDefaults
import OneTimePassword import OneTimePassword
import Base32 import Base32
import KeychainAccess import KeychainAccess
public struct AdditionField: Equatable {
public var title: String = ""
public var content: String = ""
var asString: String { return title.isEmpty ? content : title + ": " + content }
var asTuple: (String, String) { return (title, content) }
public static func == (first: AdditionField, second: AdditionField) -> Bool {
return first.asTuple == second.asTuple
}
}
public enum OtpType {
case totp, hotp, none
static func from(token: Token?) -> OtpType {
switch token?.generator.factor {
case .some(.counter):
return .hotp
case .some(.timer):
return .totp
default:
return .none
}
}
}
enum PasswordChange: Int {
case path = 0x01
case content = 0x02
case none = 0x00
}
public class Password { public class Password {
public static let OTP_KEYWORDS = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"]
public static let BLANK = " "
public static let MULTILINE_WITH_LINE_BREAK_INDICATOR = "|"
public static let MULTILINE_WITH_LINE_BREAK_SEPARATOR = "\n"
public static let MULTILINE_WITHOUT_LINE_BREAK_INDICATOR = ">"
public static let MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR = BLANK
private static let OTPAUTH = "otpauth"
private static let OTPAUTH_URL_START = "\(OTPAUTH)://"
private static let PASSWORD_KEYWORD = "password"
private static let USERNAME_KEYWORD = "username"
private static let LOGIN_KEYWORD = "login"
private static let URL_KEYWORD = "url"
private static let UNKNOWN = "unknown"
public var name: String public var name: String
public var url: URL public var url: URL
public var namePath: String { return url.deletingPathExtension().path } public var plainText: String
public var password = ""
public var changed: Int = 0 public var changed: Int = 0
public var plainText = "" public var otpType: OtpType = .none
public var plainData: Data { return plainText.data(using: .utf8)! }
public var username: String? { return getAdditionValue(withKey: Password.USERNAME_KEYWORD, caseSensitive: false) }
public var login: String? { return getAdditionValue(withKey: Password.LOGIN_KEYWORD, caseSensitive: false) }
public var urlString: String? { return getAdditionValue(withKey: Password.URL_KEYWORD, caseSensitive: false) }
private var parser = Parser(plainText: "")
private var additions = [AdditionField]() private var additions = [AdditionField]()
private var firstLineIsOTPField = false private var firstLineIsOTPField = false
private var otpToken: Token? private var otpToken: Token? {
public var otpType: OtpType { return OtpType.from(token: self.otpToken) } didSet {
otpType = OtpType(token: otpToken)
}
}
public var namePath: String {
return url.deletingPathExtension().path
}
public var password: String {
return parser.firstLine
}
public var plainData: Data {
return plainText.data(using: .utf8)!
}
public var additionsPlainText: String {
return parser.additionsSection
}
public var username: String? {
return getAdditionValue(withKey: Constants.USERNAME_KEYWORD)
}
public var login: String? {
return getAdditionValue(withKey: Constants.LOGIN_KEYWORD)
}
public var urlString: String? {
return getAdditionValue(withKey: Constants.URL_KEYWORD)
}
public init(name: String, url: URL, plainText: String) { public init(name: String, url: URL, plainText: String) {
self.name = name
self.url = url
self.initEverything(name: name, url: url, plainText: plainText)
}
public func updatePassword(name: String, url: URL, plainText: String) {
if self.plainText != plainText || self.url != url {
if self.plainText != plainText {
changed = changed|PasswordChange.content.rawValue
}
if self.url != url {
changed = changed|PasswordChange.path.rawValue
}
self.initEverything(name: name, url: url, plainText: plainText)
}
}
private func initEverything(name: String, url: URL, plainText: String) {
self.name = name self.name = name
self.url = url self.url = url
self.plainText = plainText self.plainText = plainText
additions.removeAll() initEverything()
}
// split the plain text
let plainTextSplit = self.plainText
.split(omittingEmptySubsequences: false) { $0 == "\n" || $0 == "\r\n" }
.map(String.init)
// get password public func updatePassword(name: String, url: URL, plainText: String) {
password = plainTextSplit.first ?? "" guard self.plainText != plainText || self.url != url else {
return
}
// get remaining lines (filter out empty lines) if self.plainText != plainText {
let additionalLines = plainTextSplit[1...].filter { !$0.isEmpty } self.plainText = plainText
changed = changed|PasswordChange.content.rawValue
}
if self.url != url {
self.url = url
changed = changed|PasswordChange.path.rawValue
}
// parse lines to get key-value pairs self.name = name
parseDataFrom(lines: additionalLines) initEverything()
}
// check whether the first line looks like an otp entry private func initEverything() {
parser = Parser(plainText: self.plainText)
additions = parser.additionFields
// Check whether the first line looks like an otp entry.
checkPasswordForOtpToken() checkPasswordForOtpToken()
// construct the otp token // Construct the otp token.
updateOtpToken() updateOtpToken()
} }
private func parseDataFrom(lines: [String]) {
var unknownIndex = 0
var i = lines.startIndex
while i < lines.count {
let line = lines[i]
i += 1
var (key, value) = Password.getKeyValuePair(from: line)
if key == nil {
unknownIndex += 1
key = "\(Password.UNKNOWN) \(unknownIndex)"
} else if value == Password.MULTILINE_WITH_LINE_BREAK_INDICATOR {
value = gatherMultilineValue(from: lines, startingAt: &i, removingLineBreaks: false)
} else if value == Password.MULTILINE_WITHOUT_LINE_BREAK_INDICATOR {
value = gatherMultilineValue(from: lines, startingAt: &i, removingLineBreaks: true)
}
additions.append(AdditionField(title: key!, content: value))
}
}
private func gatherMultilineValue(from content: [String], startingAt i: inout Int, removingLineBreaks: Bool) -> String {
var result = ""
guard i < content.count else { return result }
let numberInitialBlanks = content[i].enumerated().first(where: { $1 != Character(Password.BLANK) })?.0 ?? content[i].count
guard numberInitialBlanks != 0 else { return result }
let initialBlanks = String(repeating: Password.BLANK, count: numberInitialBlanks)
while i < content.count && content[i].starts(with: initialBlanks) {
result.append(String(content[i].dropFirst(numberInitialBlanks)))
result.append(removingLineBreaks ? Password.MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR : Password.MULTILINE_WITH_LINE_BREAK_SEPARATOR)
i += 1
}
return result.trimmingCharacters(in: .whitespacesAndNewlines)
}
private func checkPasswordForOtpToken() { private func checkPasswordForOtpToken() {
let (key, value) = Password.getKeyValuePair(from: self.password) let (key, value) = Parser.getKeyValuePair(from: password)
if Password.OTP_KEYWORDS.contains(key ?? "") { if Constants.OTP_KEYWORDS.contains(key ?? "") {
firstLineIsOTPField = true firstLineIsOTPField = true
self.additions.append(AdditionField(title: key!, content: value)) additions.append(key! => value)
} else { } else {
firstLineIsOTPField = false firstLineIsOTPField = false
} }
@ -170,48 +106,17 @@ public class Password {
public func getFilteredAdditions() -> [AdditionField] { public func getFilteredAdditions() -> [AdditionField] {
return additions.filter { field in return additions.filter { field in
field.title.lowercased() != Password.USERNAME_KEYWORD field.title.lowercased() != Constants.USERNAME_KEYWORD
&& field.title.lowercased() != Password.LOGIN_KEYWORD && field.title.lowercased() != Constants.LOGIN_KEYWORD
&& field.title.lowercased() != Password.PASSWORD_KEYWORD && field.title.lowercased() != Constants.PASSWORD_KEYWORD
&& (!field.title.hasPrefix(Password.UNKNOWN) || !SharedDefaults[.isHideUnknownOn]) && (!field.title.hasPrefix(Constants.UNKNOWN) || !SharedDefaults[.isHideUnknownOn])
&& (!Password.OTP_KEYWORDS.contains(field.title) || !SharedDefaults[.isHideOTPOn]) && (!Constants.OTP_KEYWORDS.contains(field.title) || !SharedDefaults[.isHideOTPOn])
} }
} }
// return a key-value pair from the line private func getAdditionValue(withKey key: String, caseSensitive: Bool = false) -> String? {
// key might be nil, if there is no ":" in the line let toLowercase = { (string: String) -> String in return caseSensitive ? string : string.lowercased() }
private static func getKeyValuePair(from line: String) -> (key: String?, value: String) { return additions.first(where: { toLowercase($0.title) == toLowercase(key) })?.content
let items = line.components(separatedBy: ": ").map{String($0).trimmingCharacters(in: .whitespaces)}
var key : String? = nil
var value = ""
if items.count == 1 || (items[0].isEmpty && items[1].isEmpty) {
// no ": " found, or empty on both sides of ": "
value = line
// otpauth special case
if value.hasPrefix(Password.OTPAUTH_URL_START) {
key = Password.OTPAUTH
}
} else {
if !items[0].isEmpty {
key = items[0]
}
value = items[1]
}
return (key, value)
}
public func getAdditionsPlainText() -> String {
// lines starting from the second
let plainTextSplit = plainText.split(maxSplits: 1, omittingEmptySubsequences: false) {
$0 == "\n" || $0 == "\r\n"
}.map(String.init)
return plainTextSplit.count == 1 ? "" : plainTextSplit[1]
}
private func getAdditionValue(withKey key: String, caseSensitive: Bool = true) -> String? {
let searchKey = caseSensitive ? key : key.lowercased()
let matchingField = additions.first { (caseSensitive ? $0.title : $0.title.lowercased()) == searchKey }
return matchingField?.content
} }
/* /*
@ -239,9 +144,9 @@ public class Password {
self.otpToken = nil self.otpToken = nil
// get otpauth, if we are able to generate a token, return // get otpauth, if we are able to generate a token, return
if var otpauthString = getAdditionValue(withKey: Password.OTPAUTH) { if var otpauthString = getAdditionValue(withKey: Constants.OTPAUTH, caseSensitive: true) {
if !otpauthString.hasPrefix("\(Password.OTPAUTH):") { if !otpauthString.hasPrefix("\(Constants.OTPAUTH):") {
otpauthString = "\(Password.OTPAUTH):\(otpauthString)" otpauthString = "\(Constants.OTPAUTH):\(otpauthString)"
} }
if let otpauthUrl = URL(string: otpauthString), if let otpauthUrl = URL(string: otpauthString),
let token = Token(url: otpauthUrl) { let token = Token(url: otpauthUrl) {
@ -358,10 +263,10 @@ public class Password {
var lines : [String] = [] var lines : [String] = []
self.plainText.enumerateLines() { line, _ in self.plainText.enumerateLines() { line, _ in
let (key, _) = Password.getKeyValuePair(from: line) let (key, _) = Parser.getKeyValuePair(from: line)
if !Password.OTP_KEYWORDS.contains(key ?? "") { if !Constants.OTP_KEYWORDS.contains(key ?? "") {
lines.append(line) lines.append(line)
} else if key == Password.OTPAUTH && newOtpauth != nil { } else if key == Constants.OTPAUTH && newOtpauth != nil {
lines.append(newOtpauth!) lines.append(newOtpauth!)
// set to nil to prevent duplication // set to nil to prevent duplication
newOtpauth = nil newOtpauth = nil
@ -376,11 +281,6 @@ public class Password {
return self.otpToken?.currentPassword return self.otpToken?.currentPassword
} }
public static func LooksLikeOTP(line: String) -> Bool {
let (key, _) = getKeyValuePair(from: line)
return Password.OTP_KEYWORDS.contains(key ?? "")
}
public static func generatePassword(length: Int) -> String{ public static func generatePassword(length: Int) -> String{
switch SharedDefaults[.passwordGeneratorFlavor] { switch SharedDefaults[.passwordGeneratorFlavor] {
case "Random": case "Random":

View file

@ -0,0 +1,47 @@
//
// AdditionField.swift
// passKit
//
// Created by Danny Moesch on 30.09.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
public struct AdditionField: Hashable {
public let title: String, content: String
var asString: String {
return title.isEmpty ? content : title + ": " + content
}
var asTuple: (String, String) {
return (title, content)
}
}
extension AdditionField {
static func | (left: String, right: AdditionField) -> String {
return left | right.asString
}
static func | (left: AdditionField, right: String) -> String {
return left.asString | right
}
static func | (left: AdditionField, right: AdditionField) -> String {
return left.asString | right
}
}
extension AdditionField: Equatable {
public static func == (first: AdditionField, second: AdditionField) -> Bool {
return first.asTuple == second.asTuple
}
}
infix operator =>: MultiplicationPrecedence
func => (key: String, value: String) -> AdditionField {
return AdditionField(title: key, content: value)
}

View file

@ -0,0 +1,47 @@
//
// Constants.swift
// passKit
//
// Created by Danny Moesch on 16.08.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
public struct Constants {
public static let OTP_KEYWORDS = [
"otp_secret",
"otp_type",
"otp_algorithm",
"otp_period",
"otp_digits",
"otp_counter",
"otpauth",
]
static let BLANK = " "
static let MULTILINE_WITH_LINE_BREAK_INDICATOR = "|"
static let MULTILINE_WITH_LINE_BREAK_SEPARATOR = "\n"
static let MULTILINE_WITHOUT_LINE_BREAK_INDICATOR = ">"
static let MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR = BLANK
static let OTPAUTH = "otpauth"
static let OTPAUTH_URL_START = "\(OTPAUTH)://"
static let PASSWORD_KEYWORD = "password"
static let USERNAME_KEYWORD = "username"
static let LOGIN_KEYWORD = "login"
static let URL_KEYWORD = "url"
static let UNKNOWN = "unknown"
public static func isOtpRelated(line: String) -> Bool {
let (key, _) = Parser.getKeyValuePair(from: line)
return OTP_KEYWORDS.contains(key ?? "")
}
static func unknown(_ number: UInt) -> String {
return "\(UNKNOWN) \(number)"
}
static func getSeparator(breakingLines: Bool) -> String {
return breakingLines ? MULTILINE_WITH_LINE_BREAK_SEPARATOR : MULTILINE_WITHOUT_LINE_BREAK_SEPARATOR
}
}

View file

@ -0,0 +1,93 @@
//
// Parser.swift
// passKit
//
// Created by Danny Moesch on 16.08.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
class Parser {
let firstLine: String
let additionsSection: String
let purgedAdditionalLines: [String]
// The parsing process is expensive. This field makes sure it is only done once if actually needed.
private(set) lazy var additionFields = getAdditionFields()
init(plainText: String) {
let splittedPlainText = plainText
.split(omittingEmptySubsequences: false) { $0 == "\n" || $0 == "\r\n" }
.map(String.init)
firstLine = splittedPlainText.first!
additionsSection = splittedPlainText[1...].joined(separator: "\n")
purgedAdditionalLines = splittedPlainText[1...].filter { !$0.isEmpty }
}
private func getAdditionFields() -> [AdditionField] {
var additions: [AdditionField] = []
var unknownIndex: UInt = 0
var i = purgedAdditionalLines.startIndex
while i < purgedAdditionalLines.count {
let line = purgedAdditionalLines[i]
i += 1
var (key, value) = Parser.getKeyValuePair(from: line)
if key == nil {
unknownIndex += 1
key = Constants.unknown(unknownIndex)
} else if value == Constants.MULTILINE_WITH_LINE_BREAK_INDICATOR {
value = gatherMultilineValue(startingAt: &i, removingLineBreaks: false)
} else if value == Constants.MULTILINE_WITHOUT_LINE_BREAK_INDICATOR {
value = gatherMultilineValue(startingAt: &i, removingLineBreaks: true)
}
additions.append(key! => value)
}
return additions
}
private func gatherMultilineValue(startingAt i: inout Int, removingLineBreaks: Bool) -> String {
var result = ""
guard i < purgedAdditionalLines.count else {
return result
}
let numberInitialBlanks = purgedAdditionalLines[i].enumerated().first {
$1 != Character(Constants.BLANK)
}?.0 ?? purgedAdditionalLines[i].count
guard numberInitialBlanks != 0 else {
return result
}
let initialBlanks = String(repeating: Constants.BLANK, count: numberInitialBlanks)
while i < purgedAdditionalLines.count && purgedAdditionalLines[i].starts(with: initialBlanks) {
result.append(String(purgedAdditionalLines[i].dropFirst(numberInitialBlanks)))
result.append(Constants.getSeparator(breakingLines: !removingLineBreaks))
i += 1
}
return result.trimmingCharacters(in: .whitespacesAndNewlines)
}
/// Split line from password file in to a key-value pair separted by `: `.
///
/// - Parameter line: Line from a password file
/// - Returns: Pair of two `String`s of which the first one can be 'nil'
static func getKeyValuePair(from line: String) -> (key: String?, value: String) {
let items = line.components(separatedBy: ": ").map { String($0).trimmingCharacters(in: .whitespaces) }
var key: String?
var value = ""
if items.count == 1 || (items[0].isEmpty && items[1].isEmpty) {
// No ': ' found, or empty on both sides of ': '.
value = line
// "otpauth" special case
if value.hasPrefix(Constants.OTPAUTH_URL_START) {
key = Constants.OTPAUTH
}
} else {
if !items[0].isEmpty {
key = items[0]
}
value = items[1]
}
return (key, value)
}
}

View file

@ -0,0 +1,29 @@
//
// PasswordHelpersTest.swift
// passKitTests
//
// Created by Danny Moesch on 30.09.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
import OneTimePassword
import XCTest
@testable import passKit
class PasswordHelpersTest: XCTestCase {
func testOtpType() {
let secret = "secret".data(using: .utf8)!
let totpGenerator = Generator(factor: .timer(period: 30.0), secret: secret, algorithm: .sha1, digits: 6)!
let totpToken = Token(name: "", issuer: "", generator: totpGenerator)
XCTAssertEqual(OtpType(token: totpToken), .totp)
let hotpGenerator = Generator(factor: .counter(4), secret: secret, algorithm: .sha1, digits: 6)!
let hotpToken = Token(name: "", issuer: "", generator: hotpGenerator)
XCTAssertEqual(OtpType(token: hotpToken), .hotp)
XCTAssertEqual(OtpType(token: nil), .none)
}
}

View file

@ -0,0 +1,35 @@
//
// StringExtensionTest.swift
// passKitTests
//
// Created by Danny Moesch on 30.09.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
import XCTest
@testable import passKit
class StringExtensionTest: XCTestCase {
func testStringByAddingPercentEncodingForRFC3986() {
[
("!#$&'()*+,/:;=?@[]^", "%21%23%24%26%27%28%29%2A%2B%2C/%3A%3B%3D?%40%5B%5D%5E"),
("-._~/?", "-._~/?"),
("A*b!c", "A%2Ab%21c"),
].forEach { unencoded, encoded in
XCTAssertEqual(unencoded.stringByAddingPercentEncodingForRFC3986(), encoded)
}
}
func testConcatenateAsLines() {
[
("a" | "b", "a\nb"),
("" | "b", "\nb"),
("a" | "", "a"),
("" | "", ""),
].forEach { concatenated, result in
XCTAssertEqual(concatenated, result)
}
}
}

View file

@ -0,0 +1,276 @@
//
// PasswordTests.swift
// passKitTests
//
// Created by Danny Moesch on 02.05.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
import XCTest
@testable import passKit
class PasswordTest: XCTestCase {
private let PASSWORD_PATH = "/path/to/password"
private let PASSWORD_URL = URL(fileURLWithPath: "/path/to/password")
private let PASSWORD_STRING = "abcd1234"
private let OTP_TOKEN = "otpauth://totp/email@email.com?secret=abcd1234"
private let SECURE_URL_FIELD = "url" => "https://secure.com"
private let INSECURE_URL_FIELD = "url" => "http://insecure.com"
private let LOGIN_FIELD = "login" => "login name"
private let USERNAME_FIELD = "username" => "some username"
private let NOTE_FIELD = "note" => "A NOTE"
private let HINT_FIELD = "some hints" => "äöüß // €³ %% −° && @²` | [{\\}],.<>"
func testUrl() {
let password = getPasswordObjectWith(content: "")
XCTAssertEqual(password.url, PASSWORD_URL)
XCTAssertEqual(password.namePath, PASSWORD_PATH)
}
func testEmptyFile() {
[
"",
"\n",
].forEach { fileContent in
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, "")
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, "")
XCTAssertTrue(password.getFilteredAdditions().isEmpty)
XCTAssertNil(password.username)
XCTAssertNil(password.urlString)
XCTAssertNil(password.login)
}
}
func testEmptyPassword() {
let fileContent = "\n\(LOGIN_FIELD.asString)"
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, "")
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, LOGIN_FIELD.asString)
XCTAssertFalse(does(password, contain: LOGIN_FIELD))
XCTAssertNil(password.username)
XCTAssertNil(password.urlString)
XCTAssertEqual(password.login, LOGIN_FIELD.content)
}
func testSimplePasswordFile() {
let additions = SECURE_URL_FIELD | LOGIN_FIELD | USERNAME_FIELD | NOTE_FIELD
let fileContent = PASSWORD_STRING | additions
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PASSWORD_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, additions)
XCTAssertTrue(does(password, contain: SECURE_URL_FIELD))
XCTAssertFalse(does(password, contain: LOGIN_FIELD))
XCTAssertFalse(does(password, contain: USERNAME_FIELD))
XCTAssertTrue(does(password, contain: NOTE_FIELD))
XCTAssertEqual(password.urlString, SECURE_URL_FIELD.content)
XCTAssertEqual(password.login, LOGIN_FIELD.content)
XCTAssertEqual(password.username, USERNAME_FIELD.content)
}
func testTwoPasswords() {
let additions = "efgh5678" | INSECURE_URL_FIELD
let fileContent = PASSWORD_STRING | additions
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PASSWORD_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, additions)
XCTAssertTrue(does(password, contain: INSECURE_URL_FIELD))
XCTAssertTrue(does(password, contain: Constants.unknown(1) => "efgh5678"))
XCTAssertNil(password.username)
XCTAssertEqual(password.urlString, INSECURE_URL_FIELD.content)
XCTAssertNil(password.login)
}
func testNoPassword() {
let fileContent = SECURE_URL_FIELD | NOTE_FIELD
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, SECURE_URL_FIELD.asString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, NOTE_FIELD.asString)
XCTAssertTrue(does(password, contain: NOTE_FIELD))
XCTAssertNil(password.username)
XCTAssertNil(password.urlString)
XCTAssertNil(password.login)
}
func testDuplicateKeys() {
let additions = SECURE_URL_FIELD | INSECURE_URL_FIELD
let fileContent = PASSWORD_STRING | additions
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PASSWORD_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, additions)
XCTAssertTrue(does(password, contain: SECURE_URL_FIELD))
XCTAssertTrue(does(password, contain: INSECURE_URL_FIELD))
XCTAssertNil(password.username)
XCTAssertEqual(password.urlString, SECURE_URL_FIELD.content)
XCTAssertNil(password.login)
}
func testUnknownKeys() {
let value1 = "value 1"
let value2 = "value 2"
let value3 = "value 3"
let value4 = "value 4"
let additions = value1 | NOTE_FIELD | value2 | value3 | SECURE_URL_FIELD | value4
let fileContent = PASSWORD_STRING | additions
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PASSWORD_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, additions)
XCTAssertTrue(does(password, contain: Constants.unknown(1) => value1))
XCTAssertTrue(does(password, contain: NOTE_FIELD))
XCTAssertTrue(does(password, contain: Constants.unknown(2) => value2))
XCTAssertTrue(does(password, contain: Constants.unknown(3) => value3))
XCTAssertTrue(does(password, contain: SECURE_URL_FIELD))
XCTAssertTrue(does(password, contain: Constants.unknown(4) => value4))
XCTAssertNil(password.username)
XCTAssertEqual(password.urlString, SECURE_URL_FIELD.content)
XCTAssertNil(password.login)
}
func testPasswordFileWithOtpToken() {
let additions = NOTE_FIELD | OTP_TOKEN
let fileContent = PASSWORD_STRING | additions
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PASSWORD_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, additions)
XCTAssertEqual(password.otpType, OtpType.totp)
XCTAssertNotNil(password.getOtp())
}
func testFirstLineIsOtpToken() {
let password = getPasswordObjectWith(content: OTP_TOKEN)
XCTAssertEqual(password.password, OTP_TOKEN)
XCTAssertEqual(password.plainData, OTP_TOKEN.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, "")
XCTAssertNil(password.username)
XCTAssertNil(password.urlString)
XCTAssertNil(password.login)
XCTAssertEqual(password.otpType, OtpType.totp)
XCTAssertNotNil(password.getOtp())
}
func testWrongOtpToken() {
let fileContent = "otpauth://htop/blabla"
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, fileContent)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertTrue(password.additionsPlainText.isEmpty)
XCTAssertEqual(password.otpType, OtpType.none)
XCTAssertNil(password.getOtp())
}
func testEmptyMultilineValues() {
let lineBreakField1 = "with line breaks" => "| \n"
let lineBreakField2 = "with line breaks" => "| \n "
let noLineBreakField = "without line breaks" => " > "
let additions = lineBreakField1 | lineBreakField2 | NOTE_FIELD | noLineBreakField
let fileContent = PASSWORD_STRING | additions
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PASSWORD_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, additions)
XCTAssertTrue(does(password, contain: lineBreakField1.title => ""))
XCTAssertTrue(does(password, contain: lineBreakField2.title => ""))
XCTAssertTrue(does(password, contain: NOTE_FIELD))
XCTAssertTrue(does(password, contain: noLineBreakField.title => ""))
}
func testMultilineValues() {
let lineBreakField = "with line breaks" => "|\n This is \n text spread over \n multiple lines! "
let noLineBreakField = "without line breaks" => " > \n This is \n text spread over\n multiple lines!"
let additions = lineBreakField | NOTE_FIELD | noLineBreakField
let fileContent = PASSWORD_STRING | additions
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PASSWORD_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, additions)
XCTAssertTrue(does(password, contain: lineBreakField.title => "This is \n text spread over \nmultiple lines!"))
XCTAssertTrue(does(password, contain: NOTE_FIELD))
XCTAssertTrue(does(password, contain: noLineBreakField.title => "This is text spread over multiple lines!"))
}
func testMultilineValuesMixed() {
let lineBreakField = "with line breaks" => "|\n This is \n \(HINT_FIELD.asString) spread over\n multiple lines!"
let noLineBreakField = "without line breaks" => " > \n This is \n | \n text spread over\nmultiple lines!"
let additions = lineBreakField | noLineBreakField | NOTE_FIELD
let fileContent = PASSWORD_STRING | additions
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PASSWORD_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.additionsPlainText, additions)
XCTAssertTrue(does(password, contain: lineBreakField.title => "This is \n\(HINT_FIELD.asString) spread over"))
XCTAssertTrue(does(password, contain: Constants.unknown(1) => " multiple lines!"))
XCTAssertTrue(does(password, contain: noLineBreakField.title => "This is | text spread over"))
XCTAssertTrue(does(password, contain: Constants.unknown(2) => "multiple lines!"))
XCTAssertTrue(does(password, contain: NOTE_FIELD))
}
func testUpdatePassword() {
let password = getPasswordObjectWith(content: "")
XCTAssertEqual(password.changed, 0)
password.updatePassword(name: "password", url: PASSWORD_URL, plainText: "")
XCTAssertEqual(password.changed, 0)
password.updatePassword(name: "", url: PASSWORD_URL, plainText: "a")
XCTAssertEqual(password.changed, 2)
password.updatePassword(name: "", url: URL(fileURLWithPath: "/some/path/"), plainText: "a")
XCTAssertEqual(password.changed, 3)
password.updatePassword(name: "", url: PASSWORD_URL, plainText: "")
XCTAssertEqual(password.changed, 3)
}
private func getPasswordObjectWith(content: String, url: URL? = nil) -> Password {
return Password(name: "password", url: url ?? PASSWORD_URL, plainText: content)
}
private func does(_ password: Password, contain field: AdditionField) -> Bool {
return password.getFilteredAdditions().contains(field)
}
}

View file

@ -0,0 +1,52 @@
//
// AdditionFieldTest.swift
// passKitTests
//
// Created by Danny Moesch on 30.09.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
import XCTest
@testable import passKit
class AdditionFieldTest: XCTestCase {
func testAdditionField() {
let field1 = "key" => "value"
let field2 = "some other key" => "some other value"
let field3 = "" => "no title"
XCTAssertEqual(field1.asString, "key: value")
XCTAssertEqual(field2.asString, "some other key: some other value")
XCTAssertEqual(field3.asString, "no title")
XCTAssertTrue(field1.asTuple == ("key", "value"))
XCTAssertTrue(field2.asTuple == ("some other key", "some other value"))
XCTAssertTrue(field3.asTuple == ("", "no title"))
}
func testAdditionFieldEquals() {
XCTAssertEqual("key" => "value", "key" => "value")
XCTAssertNotEqual("key" => "value", "key" => "some other value")
}
func testInfixAdditionFieldInitialization() {
XCTAssertEqual("key" => "value", AdditionField(title: "key", content: "value"))
}
func testAdditionFieldOperators() {
let field1 = "key" => "value"
let field2 = "some other key" => "some other value"
let field3 = "" => "no title"
XCTAssertEqual("start" | field1, "start\nkey: value")
XCTAssertEqual("" | field1, "\nkey: value")
XCTAssertEqual(field1 | "end", "key: value\nend")
XCTAssertEqual(field1 | "", "key: value")
XCTAssertEqual("start" | field1 | field2, "start\nkey: value\nsome other key: some other value")
XCTAssertEqual(field1 | field2 | "end", "key: value\nsome other key: some other value\nend")
XCTAssertEqual(field1 | field2 | field3, "key: value\nsome other key: some other value\nno title")
XCTAssertEqual("check" => "for right" | "operator" => "precedence", "check: for right\noperator: precedence")
}
}

View file

@ -0,0 +1,31 @@
//
// ConstantsTest.swift
// passKitTests
//
// Created by Danny Moesch on 30.09.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
import XCTest
@testable import passKit
class ConstantsTest: XCTestCase {
func testIsOtpRelated() {
XCTAssertTrue(Constants.isOtpRelated(line: "otpauth://something"))
XCTAssertTrue(Constants.isOtpRelated(line: "otp_algorithm: algorithm"))
XCTAssertFalse(Constants.isOtpRelated(line: "otp: something"))
XCTAssertFalse(Constants.isOtpRelated(line: "otp"))
}
func testUnknown() {
XCTAssertEqual(Constants.unknown(0), "unknown 0")
XCTAssertEqual(Constants.unknown(10), "unknown 10")
}
func testGetSeparator() {
XCTAssertEqual(Constants.getSeparator(breakingLines: true), "\n")
XCTAssertEqual(Constants.getSeparator(breakingLines: false), " ")
}
}

View file

@ -0,0 +1,102 @@
//
// ParserTest.swift
// passKitTests
//
// Created by Danny Moesch on 18.08.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
@testable import passKit
import XCTest
class ParserTest: XCTestCase {
private let FIELD = "key" => "value"
private let LOGIN_FIELD = "login" => "login name"
private let SECURE_URL_FIELD = "url" => "https://secure.com"
private let INSECURE_URL_FIELD = "url" => "http://insecure.com"
private let USERNAME_FIELD = "username" => "微 分 方 程"
private let NOTE_FIELD = "note" => "A NOTE"
private let MULTILINE_BLOCK_START = "multiline block" => "|"
private let MULTILINE_LINE_START = "multiline line" => ">"
func testInit() {
[
("", "", "", []),
("a", "a", "", []),
("a\nb", "a", "b", ["b"]),
("a\n\nb", "a", "\nb", ["b"]),
("a\r\nb", "a", "b", ["b"]),
("a\nb\nc\n\nd", "a", "b\nc\n\nd", ["b", "c", "d"]),
].forEach { plainText, firstLine, additionsSection, purgedAdditionalLines in
let parser = Parser(plainText: plainText)
XCTAssertEqual(parser.firstLine, firstLine)
XCTAssertEqual(parser.additionsSection, additionsSection)
XCTAssertEqual(parser.purgedAdditionalLines, purgedAdditionalLines)
}
}
func testGetKeyValuePair() {
XCTAssertTrue(Parser.getKeyValuePair(from: "key: value") == ("key", "value"))
XCTAssertTrue(Parser.getKeyValuePair(from: "a key: a value") == ("a key", "a value"))
XCTAssertTrue(Parser.getKeyValuePair(from: "key:value") == (nil, "key:value"))
XCTAssertTrue(Parser.getKeyValuePair(from: ": value") == (nil, "value"))
XCTAssertTrue(Parser.getKeyValuePair(from: "key: ") == ("key", ""))
XCTAssertTrue(Parser.getKeyValuePair(from: "otpauth://value") == ("otpauth", "otpauth://value"))
}
func testEmptyFiles() {
XCTAssertEqual(Parser(plainText: "").additionFields, [])
XCTAssertEqual(Parser(plainText: "\n").additionFields, [])
}
func testSimpleKeyValueLines() {
let fields0 = Parser(plainText: "" | FIELD | LOGIN_FIELD | SECURE_URL_FIELD).additionFields
let fields1 = Parser(plainText: "" | FIELD | "" | SECURE_URL_FIELD).additionFields
XCTAssertEqual(fields0, [FIELD, LOGIN_FIELD, SECURE_URL_FIELD])
XCTAssertEqual(fields1, [FIELD, SECURE_URL_FIELD])
}
func testLinesWithoutKey() {
let fields0 = Parser(plainText: "" | "value").additionFields
let fields1 = Parser(plainText: "" | LOGIN_FIELD | "value only" | INSECURE_URL_FIELD).additionFields
let fields2 = Parser(plainText: "" | LOGIN_FIELD | USERNAME_FIELD | "value:only").additionFields
let fields3 = Parser(plainText: "" | LOGIN_FIELD | "value 1" | "value 2").additionFields
XCTAssertEqual(fields0, [Constants.unknown(1) => "value"])
XCTAssertEqual(fields1, [LOGIN_FIELD, Constants.unknown(1) => "value only", INSECURE_URL_FIELD])
XCTAssertEqual(fields2, [LOGIN_FIELD, USERNAME_FIELD, Constants.unknown(1) => "value:only"])
XCTAssertEqual(fields3, [LOGIN_FIELD, Constants.unknown(1) => "value 1", Constants.unknown(2) => "value 2"])
}
func testMultilineValues() {
[
// Normal with one leading space
(" a b" | " cd", "a b\ncd", []),
// Normal with two leading spaces
(" a b" | " cd", "a b\ncd", []),
// Changing leading space lenght
(" a b" | " cd", "a b\n cd", []),
// First leading space longer than others
(" a b" | " cd", "a b", [Constants.unknown(1) => " cd"]),
// Empty first line
(" " | " cd", "cd", []),
// No leading space
("a b" | "cd", "", [Constants.unknown(1) => "a b", Constants.unknown(2) => "cd"]),
// Characters with special meaning in value
(" a: b" | " c: |" | " d", "a: b\nc: |\nd", []),
// Empty value at end
("", "", []),
// Empty value in between
("" | NOTE_FIELD, "", [NOTE_FIELD]),
].forEach { wrappedMultilineValue, content, additionalFields in
let blockField = Parser(plainText: "" | MULTILINE_BLOCK_START | wrappedMultilineValue).additionFields
XCTAssertEqual(blockField, [MULTILINE_BLOCK_START.title => content] + additionalFields)
let lineField = Parser(plainText: "" | MULTILINE_LINE_START | wrappedMultilineValue).additionFields
let contentWithoutLineBreaks = content.replacingOccurrences(of: "\n", with: " ")
XCTAssertEqual(lineField, [MULTILINE_LINE_START.title => contentWithoutLineBreaks] + additionalFields)
}
}
}

View file

@ -1,339 +0,0 @@
//
// PasswordTests.swift
// passKitTests
//
// Created by Danny Mösch on 02.05.18.
// Copyright © 2018 Bob Sun. All rights reserved.
//
import XCTest
@testable import passKit
class PasswordTest: XCTestCase {
static let EMPTY_STRING = ""
static let PASSWORD_NAME = "password"
static let PASSWORD_PATH = "/path/to/\(PASSWORD_NAME)"
static let PASSWORD_URL = URL(fileURLWithPath: PASSWORD_PATH)
static let PASSWORD_STRING = "abcd1234"
static let OTP_TOKEN = "otpauth://totp/email@email.com?secret=abcd1234"
static let SECURE_URL_FIELD = AdditionField(title: "url", content: "https://secure.com")
static let INSECURE_URL_FIELD = AdditionField(title: "url", content: "http://insecure.com")
static let LOGIN_FIELD = AdditionField(title: "login", content: "login name")
static let USERNAME_FIELD = AdditionField(title: "username", content: "some username")
static let NOTE_FIELD = AdditionField(title: "note", content: "A NOTE")
static let HINT_FIELD = AdditionField(title: "some hints", content: "äöüß // €³ %% −° && @²` | [{\\}],.<>")
func testUrl() {
let password1 = getPasswordObjectWith(content: PasswordTest.EMPTY_STRING)
XCTAssertEqual(password1.namePath, PasswordTest.PASSWORD_PATH)
}
func testLooksLikeOTP() {
XCTAssertTrue(Password.LooksLikeOTP(line: PasswordTest.OTP_TOKEN))
XCTAssertFalse(Password.LooksLikeOTP(line: "no_auth://totp/blabla"))
}
func testEmptyFile() {
let fileContent = PasswordTest.EMPTY_STRING
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PasswordTest.EMPTY_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), PasswordTest.EMPTY_STRING)
XCTAssertTrue(password.getFilteredAdditions().isEmpty)
XCTAssertNil(password.username)
XCTAssertNil(password.urlString)
XCTAssertNil(password.login)
}
func testOneEmptyLine() {
let fileContent = """
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, PasswordTest.EMPTY_STRING)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), PasswordTest.EMPTY_STRING)
XCTAssertTrue(password.getFilteredAdditions().isEmpty)
XCTAssertNil(password.username)
XCTAssertNil(password.urlString)
XCTAssertNil(password.login)
}
func testSimplePasswordFile() {
let passwordString = PasswordTest.PASSWORD_STRING
let urlField = PasswordTest.SECURE_URL_FIELD
let loginField = PasswordTest.LOGIN_FIELD
let usernameField = PasswordTest.USERNAME_FIELD
let noteField = PasswordTest.NOTE_FIELD
let fileContent = """
\(passwordString)
\(urlField.asString)
\(loginField.asString)
\(usernameField.asString)
\(noteField.asString)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, passwordString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(urlField, loginField, usernameField, noteField))
XCTAssertTrue(does(password, contain: urlField))
XCTAssertFalse(does(password, contain: loginField))
XCTAssertFalse(does(password, contain: usernameField))
XCTAssertTrue(does(password, contain: noteField))
XCTAssertEqual(password.urlString, urlField.content)
XCTAssertEqual(password.login, loginField.content)
XCTAssertEqual(password.username, usernameField.content)
}
func testTwoPasswords() {
let firstPasswordString = PasswordTest.PASSWORD_STRING
let secondPasswordString = "efgh5678"
let urlField = PasswordTest.INSECURE_URL_FIELD
let fileContent = """
\(firstPasswordString)
\(secondPasswordString)
\(urlField.asString)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, firstPasswordString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(secondPasswordString, urlField.asString))
XCTAssertTrue(does(password, contain: urlField))
XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 1", content: secondPasswordString)))
XCTAssertNil(password.username)
XCTAssertEqual(password.urlString, urlField.content)
XCTAssertNil(password.login)
}
func testNoPassword() {
let urlField = PasswordTest.SECURE_URL_FIELD
let noteField = PasswordTest.NOTE_FIELD
let fileContent = """
\(urlField.asString)
\(noteField.asString)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, urlField.asString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(noteField))
XCTAssertTrue(does(password, contain: noteField))
XCTAssertNil(password.username)
XCTAssertNil(password.urlString)
XCTAssertNil(password.login)
}
func testDuplicateKeys() {
let passwordString = PasswordTest.PASSWORD_STRING
let urlField1 = PasswordTest.SECURE_URL_FIELD
let urlField2 = PasswordTest.INSECURE_URL_FIELD
let fileContent = """
\(passwordString)
\(urlField1.asString)
\(urlField2.asString)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, passwordString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(urlField1, urlField2))
XCTAssertTrue(does(password, contain: urlField1))
XCTAssertTrue(does(password, contain: urlField2))
XCTAssertNil(password.username)
XCTAssertEqual(password.urlString, urlField1.content)
XCTAssertNil(password.login)
}
func testUnknownKeys() {
let passwordString = PasswordTest.PASSWORD_STRING
let value1 = "value 1"
let value2 = "value 2"
let value3 = "value 3"
let value4 = "value 4"
let noteField = PasswordTest.NOTE_FIELD
let urlField = PasswordTest.SECURE_URL_FIELD
let fileContent = """
\(passwordString)
\(value1)
\(noteField.asString)
\(value2)
\(value3)
\(urlField.asString)
\(value4)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, passwordString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(value1, noteField.asString, value2, value3, urlField.asString, value4))
XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 1", content: value1)))
XCTAssertTrue(does(password, contain: noteField))
XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 2", content: value2)))
XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 3", content: value3)))
XCTAssertTrue(does(password, contain: urlField))
XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 4", content: value4)))
XCTAssertNil(password.username)
XCTAssertEqual(password.urlString, urlField.content)
XCTAssertNil(password.login)
}
func testPasswordFileWithOtpToken() {
let passwordString = PasswordTest.PASSWORD_STRING
let noteField = PasswordTest.NOTE_FIELD
let otpToken = PasswordTest.OTP_TOKEN
let fileContent = """
\(passwordString)
\(noteField.asString)
\(otpToken)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, passwordString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(noteField.asString, otpToken))
XCTAssertEqual(password.otpType, OtpType.totp)
XCTAssertNotNil(password.getOtp())
}
func testFirstLineIsOtpToken() {
let otpToken = PasswordTest.OTP_TOKEN
let fileContent = """
\(otpToken)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, otpToken)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), PasswordTest.EMPTY_STRING)
XCTAssertNil(password.username)
XCTAssertNil(password.urlString)
XCTAssertNil(password.login)
XCTAssertEqual(password.otpType, OtpType.totp)
XCTAssertNotNil(password.getOtp())
}
func testWrongOtpToken() {
let otpToken = "otpauth://htop/blabla"
let fileContent = """
\(otpToken)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, otpToken)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.otpType, OtpType.none)
XCTAssertNil(password.getOtp())
}
func testEmptyMultilineValues() {
let passwordString = PasswordTest.PASSWORD_STRING
let lineBreakField1 = AdditionField(title: "with line breaks", content: "| \n")
let lineBreakField2 = AdditionField(title: "with line breaks", content: "| \n ")
let noteField = PasswordTest.NOTE_FIELD
let noLineBreakField = AdditionField(title: "without line breaks", content: " > ")
let fileContent = """
\(passwordString)
\(lineBreakField1.asString)
\(lineBreakField2.asString)
\(noteField.asString)
\(noLineBreakField.asString)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, passwordString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(lineBreakField1, lineBreakField2, noteField, noLineBreakField))
XCTAssertTrue(does(password, contain: AdditionField(title: lineBreakField1.title, content: "")))
XCTAssertTrue(does(password, contain: AdditionField(title: lineBreakField2.title, content: "")))
XCTAssertTrue(does(password, contain: noteField))
XCTAssertTrue(does(password, contain: AdditionField(title: noLineBreakField.title, content: "")))
}
func testMultilineValues() {
let passwordString = PasswordTest.PASSWORD_STRING
let noteField = PasswordTest.NOTE_FIELD
let lineBreakField = AdditionField(title: "with line breaks", content: "|\n This is \n text spread over \n multiple lines! ")
let noLineBreakField = AdditionField(title: "without line breaks", content: " > \n This is \n text spread over\n multiple lines!")
let fileContent = """
\(passwordString)
\(lineBreakField.asString)
\(noteField.asString)
\(noLineBreakField.asString)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, passwordString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(lineBreakField, noteField, noLineBreakField))
XCTAssertTrue(does(password, contain: AdditionField(title: lineBreakField.title, content: "This is \n text spread over \nmultiple lines!")))
XCTAssertTrue(does(password, contain: noteField))
XCTAssertTrue(does(password, contain: AdditionField(title: noLineBreakField.title, content: "This is text spread over multiple lines!")))
}
func testMultilineValuesMixed() {
let passwordString = PasswordTest.PASSWORD_STRING
let hintField = PasswordTest.HINT_FIELD
let noteField = PasswordTest.NOTE_FIELD
let lineBreakField = AdditionField(title: "with line breaks", content: "|\n This is \n \(hintField.asString) spread over\n multiple lines!")
let noLineBreakField = AdditionField(title: "without line breaks", content: " > \n This is \n | \n text spread over\nmultiple lines!")
let fileContent = """
\(passwordString)
\(lineBreakField.asString)
\(noLineBreakField.asString)
\(noteField.asString)
"""
let password = getPasswordObjectWith(content: fileContent)
XCTAssertEqual(password.password, passwordString)
XCTAssertEqual(password.plainData, fileContent.data(using: .utf8))
XCTAssertEqual(password.getAdditionsPlainText(), asPlainText(lineBreakField, noLineBreakField, noteField))
XCTAssertTrue(does(password, contain: AdditionField(title: lineBreakField.title, content: "This is \n\(hintField.asString) spread over")))
XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 1", content: " multiple lines!")))
XCTAssertTrue(does(password, contain: AdditionField(title: noLineBreakField.title, content: "This is | text spread over")))
XCTAssertTrue(does(password, contain: AdditionField(title: "unknown 2", content: "multiple lines!")))
XCTAssertTrue(does(password, contain: noteField))
}
private func getPasswordObjectWith(content: String, url: URL = PasswordTest.PASSWORD_URL) -> Password {
return Password(name: PasswordTest.PASSWORD_NAME, url: url, plainText: content)
}
private func does(_ password: Password, contain field: AdditionField) -> Bool {
return password.getFilteredAdditions().contains(field)
}
private func asPlainText(_ strings: String...) -> String {
return strings.joined(separator: "\n")
}
private func asPlainText(_ fields: AdditionField...) -> String {
return fields.map { $0.asString }.joined(separator: "\n")
}
}