From 22bf5a1d4e69859fd68b7e217ccd355aee584017 Mon Sep 17 00:00:00 2001 From: Joel-David <4586209+joeldavidw@users.noreply.github.com> Date: Tue, 23 Jul 2024 20:55:27 +0700 Subject: [PATCH] fix: fixed OtpAuthUrl parsing and added tests (#38) * fixed tokenToOtpAuthUrl * fixed otpauth parsing and added tests --- Chronos.xcodeproj/project.pbxproj | 4 + Chronos/Services/OTPService.swift | 22 +- ChronosTests/Token/ParseOtpAuthUrl.swift | 284 ++++++++++++++++++ .../Token/QrCodeGenerationAndParsing.swift | 4 +- ChronosTests/Token/TokenToOtpAuthUrl.swift | 4 +- 5 files changed, 310 insertions(+), 8 deletions(-) create mode 100644 ChronosTests/Token/ParseOtpAuthUrl.swift diff --git a/Chronos.xcodeproj/project.pbxproj b/Chronos.xcodeproj/project.pbxproj index 01da226..fefca57 100644 --- a/Chronos.xcodeproj/project.pbxproj +++ b/Chronos.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ 6BB37D852C483066008DA122 /* ImportFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB37D842C483066008DA122 /* ImportFailureView.swift */; }; 6BC3C3B52BA6B91E00B181B9 /* BiometricsSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC3C3B42BA6B91E00B181B9 /* BiometricsSetupView.swift */; }; 6BC5F0482C4F8B1B00BA106F /* Html in Frameworks */ = {isa = PBXBuildFile; productRef = 6BC5F0472C4F8B1B00BA106F /* Html */; }; + 6BC5F04A2C4FDE6E00BA106F /* ParseOtpAuthUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC5F0492C4FDE6E00BA106F /* ParseOtpAuthUrl.swift */; }; 6BD6D2012C11FEB4004512BF /* OTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD6D2002C11FEB4004512BF /* OTPService.swift */; }; 6BD90AA52B8E34BB00FABD91 /* PasswordLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD90AA42B8E34BB00FABD91 /* PasswordLoginView.swift */; }; 6BE122922BD6413D008636D2 /* ChronosCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE122912BD6413D008636D2 /* ChronosCrypto.swift */; }; @@ -137,6 +138,7 @@ 6BB37D822C480B07008DA122 /* ImportConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportConfirmationView.swift; sourceTree = ""; }; 6BB37D842C483066008DA122 /* ImportFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportFailureView.swift; sourceTree = ""; }; 6BC3C3B42BA6B91E00B181B9 /* BiometricsSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricsSetupView.swift; sourceTree = ""; }; + 6BC5F0492C4FDE6E00BA106F /* ParseOtpAuthUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseOtpAuthUrl.swift; sourceTree = ""; }; 6BD6D2002C11FEB4004512BF /* OTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPService.swift; sourceTree = ""; }; 6BD90AA42B8E34BB00FABD91 /* PasswordLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordLoginView.swift; sourceTree = ""; }; 6BE122912BD6413D008636D2 /* ChronosCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChronosCrypto.swift; sourceTree = ""; }; @@ -361,6 +363,7 @@ children = ( 6B8132F92C4C0F6300DB367E /* TokenToOtpAuthUrl.swift */, 6B8132FC2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift */, + 6BC5F0492C4FDE6E00BA106F /* ParseOtpAuthUrl.swift */, ); path = Token; sourceTree = ""; @@ -654,6 +657,7 @@ files = ( 6B8132FD2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift in Sources */, 6B4CBF2F2C490FB700983D44 /* Chronos.swift in Sources */, + 6BC5F04A2C4FDE6E00BA106F /* ParseOtpAuthUrl.swift in Sources */, 6B8132F22C4975DA00DB367E /* Raivo.swift in Sources */, 6B8132FA2C4C0F6300DB367E /* TokenToOtpAuthUrl.swift in Sources */, ); diff --git a/Chronos/Services/OTPService.swift b/Chronos/Services/OTPService.swift index 42c5b19..3a01728 100644 --- a/Chronos/Services/OTPService.swift +++ b/Chronos/Services/OTPService.swift @@ -63,8 +63,20 @@ public class OTPService { token.account = path } else { let label = path.split(separator: ":", maxSplits: 1).map { String($0) } - token.issuer = label[0] - token.account = label[1] + + if label.count == 2 { + token.issuer = label[0] + token.account = label[1] + } + + // Label is malformed e.g. :Account or Issuer: + if label.count == 1 { + if path.hasPrefix(":") { + token.account = label[0] + } else if path.hasSuffix(":") { + token.issuer = label[0] + } + } } } @@ -80,7 +92,7 @@ public class OTPService { case "issuer": token.issuer = item.value ?? "" case "algorithm": - token.algorithm = TokenAlgorithmEnum(rawValue: item.value?.lowercased() ?? "") ?? .SHA1 + token.algorithm = TokenAlgorithmEnum(rawValue: item.value?.uppercased() ?? "") ?? .SHA1 case "digits": token.digits = Int(item.value ?? "") ?? 6 case "counter": @@ -100,7 +112,9 @@ public class OTPService { components.scheme = "otpauth" components.host = token.type.rawValue.lowercased() - components.path = !token.issuer.isEmpty ? "/\(token.issuer):\(token.account)" : "/\(token.account)" + components.path = !token.account.isEmpty + ? (!token.issuer.isEmpty ? "/\(token.issuer):\(token.account)" : "/\(token.account)") + : "/" var queryItems: [URLQueryItem] = [ URLQueryItem(name: "secret", value: token.secret), diff --git a/ChronosTests/Token/ParseOtpAuthUrl.swift b/ChronosTests/Token/ParseOtpAuthUrl.swift new file mode 100644 index 0000000..f0c2509 --- /dev/null +++ b/ChronosTests/Token/ParseOtpAuthUrl.swift @@ -0,0 +1,284 @@ +@testable import Chronos +import XCTest + +final class ParseOtpAuthUrlTests: XCTestCase { + func testTotp() throws { + let otpService = OTPService() + + // Test case 1: Standard TOTP token + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Apple:john@appleseed.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") + XCTAssertEqual(token.issuer, "Apple") + XCTAssertEqual(token.account, "john@appleseed.com") + XCTAssertEqual(token.period, 30) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.TOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing TOTP URL failed") + } + + // Test case 2: TOTP token without account + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") + XCTAssertEqual(token.issuer, "Apple") + XCTAssertEqual(token.account, "") + XCTAssertEqual(token.period, 30) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.TOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing TOTP URL failed") + } + + // Test case 3: TOTP token with different period and algorithm + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Apple:john@appleseed.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=7&issuer=Apple&period=45") + XCTAssertEqual(token.issuer, "Apple") + XCTAssertEqual(token.account, "john@appleseed.com") + XCTAssertEqual(token.period, 45) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 7) + XCTAssertEqual(token.type, TokenTypeEnum.TOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA256) + } catch { + XCTFail("Parsing TOTP URL failed") + } + + // Test case 4: TOTP token without issuer + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/john@doe.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30") + XCTAssertEqual(token.issuer, "") + XCTAssertEqual(token.account, "john@doe.com") + XCTAssertEqual(token.period, 30) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.TOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing TOTP URL failed") + } + + // Test case 5: TOTP token with special characters in account and issuer + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/My%20Company:john.doe+test@mycompany.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=My%20Company&period=30") + XCTAssertEqual(token.issuer, "My Company") + XCTAssertEqual(token.account, "john.doe+test@mycompany.com") + XCTAssertEqual(token.period, 30) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.TOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing TOTP URL failed") + } + + // Test case 6: TOTP token with no issuer and no account + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30") + XCTAssertEqual(token.issuer, "") + XCTAssertEqual(token.account, "") + XCTAssertEqual(token.period, 30) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.TOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing TOTP URL failed") + } + + // Test case 7: TOTP token with different digits and SHA512 algorithm + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Google:alice@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512&digits=8&issuer=Google&period=60") + XCTAssertEqual(token.issuer, "Google") + XCTAssertEqual(token.account, "alice@google.com") + XCTAssertEqual(token.period, 60) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 8) + XCTAssertEqual(token.type, TokenTypeEnum.TOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA512) + } catch { + XCTFail("Parsing TOTP URL failed") + } + + // Test case 8: TOTP token with empty secret + do { + _ = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&period=30") + XCTFail("Expected OTPError.invalidSecret but no error was thrown") + } catch OTPError.invalidSecret { + // Success: expected error was thrown + } catch { + XCTFail("Expected OTPError.invalidSecret but a different error was thrown") + } + + // Test case 9: Standard TOTP token with lowercase algorithm + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha1&digits=6&issuer=GitHub&period=30") + XCTAssertEqual(token.issuer, "GitHub") + XCTAssertEqual(token.account, "user@github.com") + XCTAssertEqual(token.period, 30) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.TOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing HOTP URL failed") + } + + // Test case 10: Standard TOTP token with lowercase algorithm non default + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://totp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha256&digits=6&issuer=GitHub&period=30") + XCTAssertEqual(token.issuer, "GitHub") + XCTAssertEqual(token.account, "user@github.com") + XCTAssertEqual(token.period, 30) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.TOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA256) + } catch { + XCTFail("Parsing HOTP URL failed") + } + } + + func testHotp() throws { + let otpService = OTPService() + + // Test case 1: Standard HOTP token + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") + XCTAssertEqual(token.issuer, "GitHub") + XCTAssertEqual(token.account, "user@github.com") + XCTAssertEqual(token.counter, 1) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.HOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing HOTP URL failed") + } + + // Test case 2: HOTP token without account + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") + XCTAssertEqual(token.issuer, "GitHub") + XCTAssertEqual(token.account, "") + XCTAssertEqual(token.counter, 1) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.HOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing HOTP URL failed") + } + + // Test case 3: HOTP token with different counter and algorithm + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=7&issuer=GitHub&counter=10") + XCTAssertEqual(token.issuer, "GitHub") + XCTAssertEqual(token.account, "user@github.com") + XCTAssertEqual(token.counter, 10) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 7) + XCTAssertEqual(token.type, TokenTypeEnum.HOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA256) + } catch { + XCTFail("Parsing HOTP URL failed") + } + + // Test case 4: HOTP token without issuer + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=1") + XCTAssertEqual(token.issuer, "") + XCTAssertEqual(token.account, "user@github.com") + XCTAssertEqual(token.counter, 1) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.HOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing HOTP URL failed") + } + + // Test case 5: HOTP token with special characters in account and issuer + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/My%20Company:user+test@mycompany.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=My%20Company&counter=1") + XCTAssertEqual(token.issuer, "My Company") + XCTAssertEqual(token.account, "user+test@mycompany.com") + XCTAssertEqual(token.counter, 1) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.HOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing HOTP URL failed") + } + + // Test case 6: HOTP token with no issuer and no account + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&counter=1") + XCTAssertEqual(token.issuer, "") + XCTAssertEqual(token.account, "") + XCTAssertEqual(token.counter, 1) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.HOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing HOTP URL failed") + } + + // Test case 7: HOTP token with different digits and SHA512 algorithm + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/Google:user@google.com?secret=JBSWY3DPEHPK3PXP&algorithm=SHA512&digits=8&issuer=Google&counter=100") + XCTAssertEqual(token.issuer, "Google") + XCTAssertEqual(token.account, "user@google.com") + XCTAssertEqual(token.counter, 100) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 8) + XCTAssertEqual(token.type, TokenTypeEnum.HOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA512) + } catch { + XCTFail("Parsing HOTP URL failed") + } + + // Test case 8: HOTP token with empty secret + do { + _ = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/Example:user@example.com?secret=&algorithm=SHA1&digits=6&issuer=Example&counter=1") + XCTFail("Expected OTPError.invalidSecret but no error was thrown") + } catch OTPError.invalidSecret { + // Success: expected error was thrown + } catch { + XCTFail("Expected OTPError.invalidSecret but a different error was thrown") + } + + // Test case 9: Standard HOTP token with lowercase algorithm + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha1&digits=6&issuer=GitHub&counter=1") + XCTAssertEqual(token.issuer, "GitHub") + XCTAssertEqual(token.account, "user@github.com") + XCTAssertEqual(token.counter, 1) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.HOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA1) + } catch { + XCTFail("Parsing HOTP URL failed") + } + + // Test case 10: Standard HOTP token with lowercase algorithm non default + do { + let token = try otpService.parseOtpAuthUrl(otpAuthStr: "otpauth://hotp/GitHub:user@github.com?secret=JBSWY3DPEHPK3PXP&algorithm=sha256&digits=6&issuer=GitHub&counter=1") + XCTAssertEqual(token.issuer, "GitHub") + XCTAssertEqual(token.account, "user@github.com") + XCTAssertEqual(token.counter, 1) + XCTAssertEqual(token.secret, "JBSWY3DPEHPK3PXP") + XCTAssertEqual(token.digits, 6) + XCTAssertEqual(token.type, TokenTypeEnum.HOTP) + XCTAssertEqual(token.algorithm, TokenAlgorithmEnum.SHA256) + } catch { + XCTFail("Parsing HOTP URL failed") + } + } +} diff --git a/ChronosTests/Token/QrCodeGenerationAndParsing.swift b/ChronosTests/Token/QrCodeGenerationAndParsing.swift index 5fbb85c..79ce55d 100644 --- a/ChronosTests/Token/QrCodeGenerationAndParsing.swift +++ b/ChronosTests/Token/QrCodeGenerationAndParsing.swift @@ -35,7 +35,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { url = otpService.tokenToOtpAuthUrl(token: token2)! let qr2 = EFQRCode.generate(for: url) detectQRCode(in: UIImage(cgImage: qr2!)) { detectedUrl in - XCTAssertEqual(detectedUrl, "otpauth://totp/Apple:?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") + XCTAssertEqual(detectedUrl, "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") } // Test case 3: TOTP token with different period and algorithm @@ -163,7 +163,7 @@ final class QrCodeGenerationAndParsingTests: XCTestCase { url = otpService.tokenToOtpAuthUrl(token: token2)! let qr2 = EFQRCode.generate(for: url) detectQRCode(in: UIImage(cgImage: qr2!)) { detectedUrl in - XCTAssertEqual(detectedUrl, "otpauth://hotp/GitHub:?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") + XCTAssertEqual(detectedUrl, "otpauth://hotp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") } // Test case 3: HOTP token with different counter and algorithm diff --git a/ChronosTests/Token/TokenToOtpAuthUrl.swift b/ChronosTests/Token/TokenToOtpAuthUrl.swift index dd3cca0..bc1975d 100644 --- a/ChronosTests/Token/TokenToOtpAuthUrl.swift +++ b/ChronosTests/Token/TokenToOtpAuthUrl.swift @@ -28,7 +28,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token2.algorithm = TokenAlgorithmEnum.SHA1 url = otpService.tokenToOtpAuthUrl(token: token2)! - XCTAssertEqual(url, "otpauth://totp/Apple:?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") + XCTAssertEqual(url, "otpauth://totp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=Apple&period=30") // Test case 3: TOTP token with different period and algorithm let token3 = Token() @@ -132,7 +132,7 @@ final class TokenToOtpAuthUrlTests: XCTestCase { token2.algorithm = TokenAlgorithmEnum.SHA1 url = otpService.tokenToOtpAuthUrl(token: token2)! - XCTAssertEqual(url, "otpauth://hotp/GitHub:?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") + XCTAssertEqual(url, "otpauth://hotp/?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&issuer=GitHub&counter=1") // Test case 3: HOTP token with different counter and algorithm let token3 = Token()