From 6726a6cc64271a56dded8195ecac790bb727d10e Mon Sep 17 00:00:00 2001 From: Stylianos Tzouvaras Date: Mon, 2 Dec 2024 20:14:18 +0200 Subject: [PATCH 1/2] Localization Controller Test Cases --- .../Controller/LocalizationController.swift | 9 ++- .../DI/Assembly/ControllerAssembly.swift | 5 +- .../TestLocalizationController.swift | 61 +++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Sources/Domain/Controller/LocalizationController.swift b/Sources/Domain/Controller/LocalizationController.swift index ac7aaa8..c8e8199 100644 --- a/Sources/Domain/Controller/LocalizationController.swift +++ b/Sources/Domain/Controller/LocalizationController.swift @@ -24,15 +24,20 @@ protocol LocalizationController: Sendable { final class LocalizationControllerImpl: LocalizationController { private let config: any EudiRQESUiConfig + private let locale: Locale - init(config: any EudiRQESUiConfig) { + init( + config: any EudiRQESUiConfig, + locale: Locale + ) { self.config = config + self.locale = locale } func get(with key: LocalizableKey, args: [String]) -> String { guard !config.translations.isEmpty, - let translations = config.translations[Locale.current.identifier], + let translations = config.translations[locale.identifier], let translation = translations[key] else { return key.defaultTranslation(args: args) diff --git a/Sources/Domain/DI/Assembly/ControllerAssembly.swift b/Sources/Domain/DI/Assembly/ControllerAssembly.swift index 8e643e9..dd4f3c3 100644 --- a/Sources/Domain/DI/Assembly/ControllerAssembly.swift +++ b/Sources/Domain/DI/Assembly/ControllerAssembly.swift @@ -22,7 +22,10 @@ final class ControllerAssembly: Assembly { func assemble(container: Container) { container.register(LocalizationController.self) { r in - LocalizationControllerImpl(config: EudiRQESUi.forceConfig()) + LocalizationControllerImpl( + config: EudiRQESUi.forceConfig(), + locale: Locale.current + ) } .inObjectScope(ObjectScope.container) diff --git a/Tests/Domain/Controller/TestLocalizationController.swift b/Tests/Domain/Controller/TestLocalizationController.swift index 7b30bba..ddbb230 100644 --- a/Tests/Domain/Controller/TestLocalizationController.swift +++ b/Tests/Domain/Controller/TestLocalizationController.swift @@ -14,14 +14,75 @@ * governing permissions and limitations under the Licence. */ import XCTest +import Cuckoo @testable import EudiRQESUi final class TestLocalizationController: XCTestCase { + var config: MockEudiRQESUiConfig! + var controller: LocalizationController! + override func setUp() { + self.config = MockEudiRQESUiConfig() + self.controller = LocalizationControllerImpl( + config: config, + locale: .init(identifier: "en_US") + ) } override func tearDown() { + self.config = nil + self.controller = nil + } + + func testGet_WhenTranslationIsAvailableWithoutArgs_ReturnsStringTranslation() { + // Given + let customGenericErrorTranslation = "CustomGenericError" + stub(config) { mock in + when(mock.translations.get).thenReturn( + ["en_US": [.genericErrorMessage: customGenericErrorTranslation]] + ) + } + + let result = self.controller.get(with: .genericErrorMessage) + + XCTAssertEqual(result, customGenericErrorTranslation) + } + + func testGet_WhenTranslationIsAvailableWithoutArgs_ReturnsLocalizedStringKeyTranslation() { + // Given + let customGenericErrorTranslation = "CustomGenericError" + stub(config) { mock in + when(mock.translations.get).thenReturn( + ["en_US": [.genericErrorMessage: customGenericErrorTranslation]] + ) + } + + let result: LocalizedStringKey = self.controller.get(with: .genericErrorMessage, args: []) + + XCTAssertEqual(result, customGenericErrorTranslation.toLocalizedStringKey) + } + + func testGet_WhenTranslationIsNotAvailableWithoutArgs_ReturnsDefaultStringTranslation() { + // Given + stub(config) { mock in + when(mock.translations.get).thenReturn([:]) + } + + let result = self.controller.get(with: .genericErrorMessage) + + XCTAssertEqual(result, LocalizableKey.genericErrorMessage.defaultTranslation(args: [])) + } + + func testGet_WhenTranslationIsNotAvailableWithArgs_ReturnsDefaultStringTranslation() { + // Given + stub(config) { mock in + when(mock.translations.get).thenReturn([:]) + } + + let result: String = self.controller.get(with: .signedBy, args: ["NISCY"]) + + XCTAssertEqual(result, LocalizableKey.signedBy.defaultTranslation(args: ["NISCY"])) } } From e5752231e223dc48e00032a4be671a7177e29bce Mon Sep 17 00:00:00 2001 From: Stylianos Tzouvaras Date: Mon, 2 Dec 2024 23:06:19 +0200 Subject: [PATCH 2/2] Completed All Interactor, Controllers tests, Separated the Kit Logic to a separate controller in order to be able to test the interactor. --- .../Domain/Controller/RQESController.swift | 83 ++++ .../DI/Assembly/ControllerAssembly.swift | 5 + .../DI/Assembly/InteractorAssembly.swift | 5 +- .../Domain/Interactor/RQESInteractor.swift | 42 +- Sources/Infrastructure/EudiRQESUi.swift | 14 +- .../Interactor/TestRQESInteractor.swift | 422 ++++++++++++++++++ Tests/Mock/GeneratedMocks.swift | 291 ++++++++++++ Tests/Util/TestConstants.swift | 103 +++++ fastlane/.xcovignore | 1 + 9 files changed, 932 insertions(+), 34 deletions(-) create mode 100644 Sources/Domain/Controller/RQESController.swift diff --git a/Sources/Domain/Controller/RQESController.swift b/Sources/Domain/Controller/RQESController.swift new file mode 100644 index 0000000..a4eef80 --- /dev/null +++ b/Sources/Domain/Controller/RQESController.swift @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European + * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work + * except in compliance with the Licence. + * + * You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the Licence for the specific language + * governing permissions and limitations under the Licence. + */ +import RqesKit +import Foundation + +protocol RQESController: Sendable { + func getRSSPMetadata() async throws -> RSSPMetadata + func getServiceAuthorizationUrl() async throws -> URL + func authorizeService(_ authorizationCode: String) async throws -> RQESServiceAuthorized + func authorizeCredential(_ authorizationCode: String) async throws -> RQESServiceCredentialAuthorized + func signDocuments(_ authorizationCode: String) async throws -> [Document] + func getCredentialsList() async throws -> [CredentialInfo] + func getCredentialAuthorizationUrl(credentialInfo: CredentialInfo, documents: [Document]) async throws -> URL +} + +final class RQESControllerImpl: RQESController { + + private let rqesUi: EudiRQESUi + + init(rqesUi: EudiRQESUi) { + self.rqesUi = rqesUi + } + + func getRSSPMetadata() async throws -> RSSPMetadata { + guard let rqesService = await self.rqesUi.getRQESService() else { + throw EudiRQESUiError.noRQESServiceProvided + } + return try await rqesService.getRSSPMetadata() + } + + func getServiceAuthorizationUrl() async throws -> URL { + guard let rqesService = await self.rqesUi.getRQESService() else { + throw EudiRQESUiError.noRQESServiceProvided + } + return try await rqesService.getServiceAuthorizationUrl() + } + + func authorizeService(_ authorizationCode: String) async throws -> RQESServiceAuthorized { + guard let rqesService = await self.rqesUi.getRQESService() else { + throw EudiRQESUiError.noRQESServiceProvided + } + return try await rqesService.authorizeService(authorizationCode: authorizationCode) + } + + func authorizeCredential(_ authorizationCode: String) async throws -> RQESServiceCredentialAuthorized { + guard let rqesService = await self.rqesUi.getRQESServiceAuthorized() else { + throw EudiRQESUiError.noRQESServiceProvided + } + return try await rqesService.authorizeCredential(authorizationCode: authorizationCode) + } + + func signDocuments(_ authorizationCode: String) async throws -> [Document] { + let authorized = try await authorizeCredential(authorizationCode) + return try await authorized.signDocuments() + } + + func getCredentialsList() async throws -> [CredentialInfo] { + guard let rqesService = await self.rqesUi.getRQESServiceAuthorized() else { + throw EudiRQESUiError.noRQESServiceProvided + } + return try await rqesService.getCredentialsList() + } + + func getCredentialAuthorizationUrl(credentialInfo: CredentialInfo, documents: [Document]) async throws -> URL { + guard let rqesService = await self.rqesUi.getRQESServiceAuthorized() else { + throw EudiRQESUiError.noRQESServiceProvided + } + return try await rqesService.getCredentialAuthorizationUrl(credentialInfo: credentialInfo, documents: documents) + } +} diff --git a/Sources/Domain/DI/Assembly/ControllerAssembly.swift b/Sources/Domain/DI/Assembly/ControllerAssembly.swift index dd4f3c3..4a06db7 100644 --- a/Sources/Domain/DI/Assembly/ControllerAssembly.swift +++ b/Sources/Domain/DI/Assembly/ControllerAssembly.swift @@ -38,5 +38,10 @@ final class ControllerAssembly: Assembly { PreferencesControllerImpl() } .inObjectScope(ObjectScope.transient) + + container.register(RQESController.self) { r in + RQESControllerImpl(rqesUi: EudiRQESUi.forceInstance()) + } + .inObjectScope(ObjectScope.transient) } } diff --git a/Sources/Domain/DI/Assembly/InteractorAssembly.swift b/Sources/Domain/DI/Assembly/InteractorAssembly.swift index 1c286b2..ea70006 100644 --- a/Sources/Domain/DI/Assembly/InteractorAssembly.swift +++ b/Sources/Domain/DI/Assembly/InteractorAssembly.swift @@ -21,7 +21,10 @@ final class InteractorAssembly: Assembly { func assemble(container: Container) { container.register(RQESInteractor.self) { r in - RQESInteractorImpl(rqesUi: EudiRQESUi.forceInstance()) + RQESInteractorImpl( + rqesUi: EudiRQESUi.forceInstance(), + rqesController: r.force(RQESController.self) + ) } .inObjectScope(ObjectScope.transient) } diff --git a/Sources/Domain/Interactor/RQESInteractor.swift b/Sources/Domain/Interactor/RQESInteractor.swift index d1aeae7..5f8d0e9 100644 --- a/Sources/Domain/Interactor/RQESInteractor.swift +++ b/Sources/Domain/Interactor/RQESInteractor.swift @@ -32,15 +32,18 @@ protocol RQESInteractor: Sendable { final class RQESInteractorImpl: RQESInteractor { private let rqesUi: EudiRQESUi + private let rqesController: RQESController - init(rqesUi: EudiRQESUi) { + init(rqesUi: EudiRQESUi, rqesController: RQESController) { self.rqesUi = rqesUi + self.rqesController = rqesController } func createRQESService(_ qtsp: QTSPData) async throws { let rQESConfig = await rqesUi.getRQESConfig() guard - let fileExtension = await getSession()?.document?.uri.pathExtension + let fileExtension = await getSession()?.document?.uri.pathExtension, + fileExtension.isEmpty == false else { throw EudiRQESUiError.noDocumentProvided } @@ -62,10 +65,8 @@ final class RQESInteractorImpl: RQESInteractor { func signDocument() async throws -> Document? { let authorizationCode = await self.getSession()?.code - let rQESServiceAuthorized = await self.rqesUi.getRQESServiceAuthorized() - if let authorizationCode, let rQESServiceAuthorized { - let authorizedCredential = try await rQESServiceAuthorized.authorizeCredential(authorizationCode: authorizationCode) - let signedDocuments = try await authorizedCredential.signDocuments() + if let authorizationCode { + let signedDocuments = try await rqesController.signDocuments(authorizationCode) return signedDocuments.first } else { throw EudiRQESUiError.unableToSignHashDocument @@ -93,17 +94,15 @@ final class RQESInteractorImpl: RQESInteractor { } func openAuthrorizationURL() async throws -> URL { - guard let rqesService = await self.rqesUi.getRQESService() else { - throw EudiRQESUiError.noRQESServiceProvided - } - let _ = try await rqesService.getRSSPMetadata() - let authorizationUrl = try await rqesService.getServiceAuthorizationUrl() + let _ = try await rqesController.getRSSPMetadata() + let authorizationUrl = try await rqesController.getServiceAuthorizationUrl() return authorizationUrl } func openCredentialAuthrorizationURL() async throws -> URL { if let uri = await self.getSession()?.document?.uri, let certificate = await self.getSession()?.certificate { + let unsignedDocuments = [ Document( id: UUID().uuidString, @@ -111,33 +110,24 @@ final class RQESInteractorImpl: RQESInteractor { ) ] - let credentialAuthorizationUrl = try await self.rqesUi.getRQESServiceAuthorized()?.getCredentialAuthorizationUrl( + let credentialAuthorizationUrl = try await rqesController.getCredentialAuthorizationUrl( credentialInfo: certificate, documents: unsignedDocuments ) + return credentialAuthorizationUrl - if let credentialAuthorizationUrl { - return credentialAuthorizationUrl - } else { - throw EudiRQESUiError.unableToOpenURL - } } else { throw EudiRQESUiError.noDocumentProvided } } func fetchCredentials() async throws -> Result<[CredentialInfo], any Error> { - if let rqesService = await self.rqesUi.getRQESService(), - let authorizationCode = await self.getSession()?.code { + if let authorizationCode = await self.getSession()?.code { do { - let rQESServiceAuthorized = try await rqesService.authorizeService(authorizationCode: authorizationCode) + let rQESServiceAuthorized = try await rqesController.authorizeService(authorizationCode) await self.rqesUi.setRQESServiceAuthorized(rQESServiceAuthorized) - let credentials = try? await self.rqesUi.getRQESServiceAuthorized()?.getCredentialsList() - if let credentials { - return .success(credentials) - } else { - return .failure(EudiRQESUiError.unableToFetchCredentials) - } + let credentials = try await rqesController.getCredentialsList() + return .success(credentials) } catch { return .failure(error) } diff --git a/Sources/Infrastructure/EudiRQESUi.swift b/Sources/Infrastructure/EudiRQESUi.swift index 6c11318..e011817 100644 --- a/Sources/Infrastructure/EudiRQESUi.swift +++ b/Sources/Infrastructure/EudiRQESUi.swift @@ -27,7 +27,7 @@ public final actor EudiRQESUi { private var session = SessionData() private static var _rqesService: RQESService? - private static var _rQESServiceAuthorized: RQESServiceAuthorized? + private static var _rqesServiceAuthorized: RQESServiceAuthorized? @discardableResult public init(config: any EudiRQESUiConfig) { @@ -41,18 +41,18 @@ public final actor EudiRQESUi { config: any EudiRQESUiConfig, router: any RouterGraph, state: State = .none, - selection: SessionData = .init(), + session: SessionData = .init(), rqesService: RQESService? = nil, - rQESServiceAuthorized: RQESServiceAuthorized? = nil + rqesServiceAuthorized: RQESServiceAuthorized? = nil ) { DIGraph.shared.load() self.router = router - self.session = selection + self.session = session Self._config = config Self._state = state Self._shared = self Self._rqesService = rqesService - Self._rQESServiceAuthorized = rQESServiceAuthorized + Self._rqesServiceAuthorized = rqesServiceAuthorized } @MainActor @@ -191,11 +191,11 @@ extension EudiRQESUi { } func getRQESServiceAuthorized() -> RQESServiceAuthorized? { - Self._rQESServiceAuthorized + Self._rqesServiceAuthorized } func setRQESServiceAuthorized(_ service: RQESServiceAuthorized?) { - Self._rQESServiceAuthorized = service + Self._rqesServiceAuthorized = service } func getRQESConfig() -> RqesServiceConfig { diff --git a/Tests/Domain/Interactor/TestRQESInteractor.swift b/Tests/Domain/Interactor/TestRQESInteractor.swift index e65c29e..feeecea 100644 --- a/Tests/Domain/Interactor/TestRQESInteractor.swift +++ b/Tests/Domain/Interactor/TestRQESInteractor.swift @@ -14,14 +14,436 @@ * governing permissions and limitations under the Licence. */ import XCTest +import Cuckoo +import RqesKit @testable import EudiRQESUi final class TestRQESInteractor: XCTestCase { + var eudiRQESUi: EudiRQESUi! + var config: MockEudiRQESUiConfig! + var rqesController: MockRQESController! + var interactor: RQESInteractorImpl! + override func setUp() { + self.config = MockEudiRQESUiConfig() + self.rqesController = MockRQESController() + self.eudiRQESUi = .init( + config: config, + router: MockRouterGraph(), + session: TestConstants.mockSession + ) + self.interactor = RQESInteractorImpl( + rqesUi: eudiRQESUi, + rqesController: rqesController + ) } override func tearDown() { + self.eudiRQESUi = nil + self.rqesController = nil + self.config = nil + self.interactor = nil + } + + func testCreateRQESService_WhenQtspSelected_ThenCreateAndCacheService() async throws { + // Given + stub(config) { mock in + when(mock.rQESConfig.get).thenReturn( + TestConstants.mockRqesService + ) + } + // When + try await interactor.createRQESService(TestConstants.mockQtspData) + + // Then + let service = await eudiRQESUi.getRQESService() + XCTAssertNotNil(service) + } + + func testCreateRQESService_WhenQtspSelectedButDocumentDataIsNotCached_ThenThrowError() async throws { + // Given + let fileUri = try XCTUnwrap(URL(string: "rqes://no_extension")) + + stub(config) { mock in + when(mock.rQESConfig.get).thenReturn( + TestConstants.mockRqesService + ) + } + eudiRQESUi = .init( + config: config, + router: MockRouterGraph(), + session: .init(document: .init(documentName: "test", uri: fileUri)) + ) + + interactor = RQESInteractorImpl( + rqesUi: eudiRQESUi, + rqesController: rqesController + ) + + // When + do { + try await interactor.createRQESService(TestConstants.mockQtspData) + XCTFail("Error should be thrown here") + } + catch { + // Then + XCTAssertEqual(error.localizedDescription, EudiRQESUiError.noDocumentProvided.localizedDescription) + } + } + + func testGetSession_WhenSessionIsCached_ThenReturnSession() async { + // When + let session = await interactor.getSession() + // Then + XCTAssertNotNil(session) + } + + func testGetQTSps_WhenConfigHasValues_ThenReturnQtsps() async { + // Given + stub(config) { mock in + when(mock.rssps.get).thenReturn( + [TestConstants.mockQtspData] + ) + } + + // When + let qtsps = await interactor.getQTSps() + // Then + XCTAssertTrue(!qtsps.isEmpty) + } + + func testFetchCredentials_WhenCodeIsCached_ThenReturnCredentialInfoArray() async throws { + // Given + let expectedAuthCode = "12345" + let mockAuthService = await TestConstants.getMockAuthorizedService() + let expectedCredentialInfo = try await TestConstants.getCredentialInfo() + + stub(rqesController) { mock in + when(mock.authorizeService(any())).thenReturn( + mockAuthService + ) + } + + stub(rqesController) { mock in + when(mock.getCredentialsList()).thenReturn( + [expectedCredentialInfo] + ) + } + + eudiRQESUi = .init( + config: config, + router: MockRouterGraph(), + session: .init(code: expectedAuthCode) + ) + + interactor = RQESInteractorImpl( + rqesUi: eudiRQESUi, + rqesController: rqesController + ) + + // When + let credentials = try await interactor.fetchCredentials().get() + + // Then + XCTAssertFalse(credentials.isEmpty) + + let service = await eudiRQESUi.getRQESServiceAuthorized() + XCTAssertNotNil(service) + } + + func testFetchCredentials_WhenCodeIsCachedButServiceThrowsError_ThenReturnError() async { + // Given + let expectedAuthCode = "12345" + let mockAuthService = await TestConstants.getMockAuthorizedService() + + stub(rqesController) { mock in + when(mock.authorizeService(any())).thenReturn( + mockAuthService + ) + } + + stub(rqesController) { mock in + when(mock.getCredentialsList()).thenThrow(EudiRQESUiError.unableToFetchCredentials) + } + + eudiRQESUi = .init( + config: config, + router: MockRouterGraph(), + session: .init(code: expectedAuthCode) + ) + + interactor = RQESInteractorImpl( + rqesUi: eudiRQESUi, + rqesController: rqesController + ) + + // When + do { + let _ = try await interactor.fetchCredentials().get() + XCTFail("Error should be thrown here") + } + catch { + // Then + XCTAssertEqual(error.localizedDescription, EudiRQESUiError.unableToFetchCredentials.localizedDescription) + } + } + + func testFetchCredentials_WhenCodeIsNotCached_ThenReturnError() async { + // When + do { + let _ = try await interactor.fetchCredentials().get() + XCTFail("Error should be thrown here") + } + catch { + // Then + XCTAssertEqual(error.localizedDescription, EudiRQESUiError.unableToFetchCredentials.localizedDescription) + } + } + + func testUpdateQTSP_WhenValuePassed_ThenVerifyCachedSelectedQtsp() async { + // Given + let expected = TestConstants.mockQtspData + + // When + await interactor.updateQTSP(expected) + + // Then + let stored = await eudiRQESUi.getSessionData().qtsp + XCTAssertEqual(expected, stored) + } + + func testUpdateDocument_WhenValuePassed_ThenVerifyCachedDocument() async throws { + // Given + let newFileUrl = try XCTUnwrap(URL(string: "file://new_path/test.pdf")) + let expected = DocumentData( + documentName: "test.pdf", + uri: newFileUrl + ) + + // When + await interactor.updateDocument(newFileUrl) + + // Then + let stored = await eudiRQESUi.getSessionData().document + XCTAssertEqual(expected, stored) + } + + func testSaveCertificate_WhenValuePassed_ThenVerifyCachedCertificate() async throws { + // Given + let expected = try await TestConstants.getCredentialInfo() + + // When + await interactor.saveCertificate(expected) + + // Then + let stored = await eudiRQESUi.getSessionData().certificate + XCTAssertNotNil(stored) + } + + func testOpenAuthrorizationURL_WhenMetaDataAndAuthUrlReturnFromService_ThenVerifyUrl() async throws { + // Given + let mockMetadata = try await TestConstants.getMetaData() + let expectedUrl = try XCTUnwrap(URL(string: "rqes://url")) + + stub(rqesController) { mock in + when(mock.getRSSPMetadata()).thenReturn( + mockMetadata + ) + } + + stub(rqesController) { mock in + when(mock.getServiceAuthorizationUrl()).thenReturn( + expectedUrl + ) + } + + // When + let result = try await interactor.openAuthrorizationURL() + + // Then + XCTAssertEqual(result, expectedUrl) + } + + func testOpenAuthrorizationURL_WhenMetaDataApiThrowsError_ThenThrowError() async { + // Given + stub(rqesController) { mock in + when(mock.getRSSPMetadata()).thenThrow(EudiRQESUiError.unableToOpenURL) + } + + // When + do { + let _ = try await interactor.openAuthrorizationURL() + XCTFail("Error should be thrown here") + } catch { + // Then + XCTAssertEqual(error.localizedDescription, EudiRQESUiError.unableToOpenURL.localizedDescription) + } + } + + func testOpenCredentialAuthrorizationURL_WhenSessionIsCachedAndValid_ThenReturnValidUrl() async throws { + // Given + let mockCredentialInfo = try await TestConstants.getCredentialInfo() + let expectedUrl = try XCTUnwrap(URL(string: "rqes://url")) + + stub(rqesController) { mock in + when(mock.getCredentialAuthorizationUrl(credentialInfo: any(), documents: any())).thenReturn(expectedUrl) + } + + eudiRQESUi = .init( + config: config, + router: MockRouterGraph(), + session: .init( + document: TestConstants.mockDocumentData, + certificate: mockCredentialInfo + ) + ) + + interactor = RQESInteractorImpl( + rqesUi: eudiRQESUi, + rqesController: rqesController + ) + + // When + let result = try await interactor.openCredentialAuthrorizationURL() + + // Then + XCTAssertEqual(result, expectedUrl) + + } + + func testOpenCredentialAuthrorizationURL_WhenSessionIsCachedButCertificateIsNil_ThenThrowError() async { + // When + do { + let _ = try await interactor.openCredentialAuthrorizationURL() + XCTFail("Error should be thrown here") + } catch { + // Then + XCTAssertEqual(error.localizedDescription, EudiRQESUiError.noDocumentProvided.localizedDescription) + } + + } + + func testOpenCredentialAuthrorizationURL_WhenSessionIsCachedAndValidButServiceReturnsError_ThenThrowError() async throws { + // Given + let mockCredentialInfo = try await TestConstants.getCredentialInfo() + + stub(rqesController) { mock in + when(mock.getCredentialAuthorizationUrl(credentialInfo: any(), documents: any())).thenThrow(EudiRQESUiError.unableToFetchCredentials) + } + + eudiRQESUi = .init( + config: config, + router: MockRouterGraph(), + session: .init( + document: TestConstants.mockDocumentData, + certificate: mockCredentialInfo + ) + ) + + interactor = RQESInteractorImpl( + rqesUi: eudiRQESUi, + rqesController: rqesController + ) + + // When + do { + let _ = try await interactor.openCredentialAuthrorizationURL() + XCTFail("Error should be thrown here") + } catch { + // Then + XCTAssertEqual(error.localizedDescription, EudiRQESUiError.unableToFetchCredentials.localizedDescription) + } + + } + + func testSignDocument_WhenSessionIsValidAndAuthCodeExists_ThenReturnSignedDocument() async throws { + // Given + let expectedAuthCode = "12345" + let mockAuthService = await TestConstants.getMockAuthorizedService() + let expectedUrl = try XCTUnwrap(URL(string: "rqes://file_path/file.pdf")) + let expectedSignedDocument: Document = .init(id: "id", fileURL: expectedUrl) + + + stub(rqesController) { mock in + when(mock.authorizeService(any())).thenReturn( + mockAuthService + ) + } + + stub(rqesController) { mock in + when(mock.signDocuments(any())).thenReturn( + [ + .init(id: "id", fileURL: expectedUrl), + .init(id: "id2", fileURL: expectedUrl) + ] + ) + } + + eudiRQESUi = .init( + config: config, + router: MockRouterGraph(), + session: .init(code: expectedAuthCode) + ) + + interactor = RQESInteractorImpl( + rqesUi: eudiRQESUi, + rqesController: rqesController + ) + + // When + let result = try await interactor.signDocument() + + // Then + XCTAssertEqual(result?.id, expectedSignedDocument.id) + } + + func testSignDocument_WhenSessionIsValidButAuthCodeDoesNotExists_ThenThrowError() async { + // When + do { + let _ = try await interactor.signDocument() + XCTFail("Error should be thrown here") + } catch { + // Then + XCTAssertEqual(error.localizedDescription, EudiRQESUiError.unableToSignHashDocument.localizedDescription) + } + } + + func testSignDocument_WhenSessionIsValidAndAuthCodeExistsButServiceThrowsError_ThenThrowError() async throws { + // Given + let expectedAuthCode = "12345" + let mockAuthService = await TestConstants.getMockAuthorizedService() + + + stub(rqesController) { mock in + when(mock.authorizeService(any())).thenReturn( + mockAuthService + ) + } + + stub(rqesController) { mock in + when(mock.signDocuments(any())).thenThrow(EudiRQESUiError.unableToSignHashDocument) + } + + eudiRQESUi = .init( + config: config, + router: MockRouterGraph(), + session: .init(code: expectedAuthCode) + ) + + interactor = RQESInteractorImpl( + rqesUi: eudiRQESUi, + rqesController: rqesController + ) + + // When + do { + let _ = try await interactor.signDocument() + XCTFail("Error should be thrown here") + } catch { + // Then + XCTAssertEqual(error.localizedDescription, EudiRQESUiError.unableToSignHashDocument.localizedDescription) + } } } diff --git a/Tests/Mock/GeneratedMocks.swift b/Tests/Mock/GeneratedMocks.swift index acce6c3..cfb065c 100644 --- a/Tests/Mock/GeneratedMocks.swift +++ b/Tests/Mock/GeneratedMocks.swift @@ -594,6 +594,297 @@ class PreferencesControllerStub:PreferencesController, @unchecked Sendable { +// MARK: - Mocks generated from file: 'Sources/Domain/Controller/RQESController.swift' + +import Cuckoo +import RqesKit +import Foundation +@testable import EudiRQESUi + +class MockRQESController: RQESController, Cuckoo.ProtocolMock, @unchecked Sendable { + typealias MocksType = RQESController + typealias Stubbing = __StubbingProxy_RQESController + typealias Verification = __VerificationProxy_RQESController + + // Original typealiases + + let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: false) + + private var __defaultImplStub: (any RQESController)? + + func enableDefaultImplementation(_ stub: any RQESController) { + __defaultImplStub = stub + cuckoo_manager.enableDefaultStubImplementation() + } + + + func getRSSPMetadata() async throws -> RSSPMetadata { + return try await cuckoo_manager.callThrows( + "getRSSPMetadata() async throws -> RSSPMetadata", + parameters: (), + escapingParameters: (), + superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall(), + defaultCall: await __defaultImplStub!.getRSSPMetadata() + ) + } + + func getServiceAuthorizationUrl() async throws -> URL { + return try await cuckoo_manager.callThrows( + "getServiceAuthorizationUrl() async throws -> URL", + parameters: (), + escapingParameters: (), + superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall(), + defaultCall: await __defaultImplStub!.getServiceAuthorizationUrl() + ) + } + + func authorizeService(_ p0: String) async throws -> RQESServiceAuthorized { + return try await cuckoo_manager.callThrows( + "authorizeService(_ p0: String) async throws -> RQESServiceAuthorized", + parameters: (p0), + escapingParameters: (p0), + superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall(), + defaultCall: await __defaultImplStub!.authorizeService(p0) + ) + } + + func authorizeCredential(_ p0: String) async throws -> RQESServiceCredentialAuthorized { + return try await cuckoo_manager.callThrows( + "authorizeCredential(_ p0: String) async throws -> RQESServiceCredentialAuthorized", + parameters: (p0), + escapingParameters: (p0), + superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall(), + defaultCall: await __defaultImplStub!.authorizeCredential(p0) + ) + } + + func signDocuments(_ p0: String) async throws -> [Document] { + return try await cuckoo_manager.callThrows( + "signDocuments(_ p0: String) async throws -> [Document]", + parameters: (p0), + escapingParameters: (p0), + superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall(), + defaultCall: await __defaultImplStub!.signDocuments(p0) + ) + } + + func getCredentialsList() async throws -> [CredentialInfo] { + return try await cuckoo_manager.callThrows( + "getCredentialsList() async throws -> [CredentialInfo]", + parameters: (), + escapingParameters: (), + superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall(), + defaultCall: await __defaultImplStub!.getCredentialsList() + ) + } + + func getCredentialAuthorizationUrl(credentialInfo p0: CredentialInfo, documents p1: [Document]) async throws -> URL { + return try await cuckoo_manager.callThrows( + "getCredentialAuthorizationUrl(credentialInfo p0: CredentialInfo, documents p1: [Document]) async throws -> URL", + parameters: (p0, p1), + escapingParameters: (p0, p1), + superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall(), + defaultCall: await __defaultImplStub!.getCredentialAuthorizationUrl(credentialInfo: p0, documents: p1) + ) + } + + struct __StubbingProxy_RQESController: Cuckoo.StubbingProxy { + private let cuckoo_manager: Cuckoo.MockManager + + init(manager: Cuckoo.MockManager) { + self.cuckoo_manager = manager + } + + func getRSSPMetadata() -> Cuckoo.ProtocolStubThrowingFunction<(), RSSPMetadata> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockRQESController.self, + method: "getRSSPMetadata() async throws -> RSSPMetadata", + parameterMatchers: matchers + )) + } + + func getServiceAuthorizationUrl() -> Cuckoo.ProtocolStubThrowingFunction<(), URL> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockRQESController.self, + method: "getServiceAuthorizationUrl() async throws -> URL", + parameterMatchers: matchers + )) + } + + func authorizeService(_ p0: M1) -> Cuckoo.ProtocolStubThrowingFunction<(String), RQESServiceAuthorized> where M1.MatchedType == String { + let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: p0) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockRQESController.self, + method: "authorizeService(_ p0: String) async throws -> RQESServiceAuthorized", + parameterMatchers: matchers + )) + } + + func authorizeCredential(_ p0: M1) -> Cuckoo.ProtocolStubThrowingFunction<(String), RQESServiceCredentialAuthorized> where M1.MatchedType == String { + let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: p0) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockRQESController.self, + method: "authorizeCredential(_ p0: String) async throws -> RQESServiceCredentialAuthorized", + parameterMatchers: matchers + )) + } + + func signDocuments(_ p0: M1) -> Cuckoo.ProtocolStubThrowingFunction<(String), [Document]> where M1.MatchedType == String { + let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: p0) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockRQESController.self, + method: "signDocuments(_ p0: String) async throws -> [Document]", + parameterMatchers: matchers + )) + } + + func getCredentialsList() -> Cuckoo.ProtocolStubThrowingFunction<(), [CredentialInfo]> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockRQESController.self, + method: "getCredentialsList() async throws -> [CredentialInfo]", + parameterMatchers: matchers + )) + } + + func getCredentialAuthorizationUrl(credentialInfo p0: M1, documents p1: M2) -> Cuckoo.ProtocolStubThrowingFunction<(CredentialInfo, [Document]), URL> where M1.MatchedType == CredentialInfo, M2.MatchedType == [Document] { + let matchers: [Cuckoo.ParameterMatcher<(CredentialInfo, [Document])>] = [wrap(matchable: p0) { $0.0 }, wrap(matchable: p1) { $0.1 }] + return .init(stub: cuckoo_manager.createStub(for: MockRQESController.self, + method: "getCredentialAuthorizationUrl(credentialInfo p0: CredentialInfo, documents p1: [Document]) async throws -> URL", + parameterMatchers: matchers + )) + } + } + + struct __VerificationProxy_RQESController: Cuckoo.VerificationProxy { + private let cuckoo_manager: Cuckoo.MockManager + private let callMatcher: Cuckoo.CallMatcher + private let sourceLocation: Cuckoo.SourceLocation + + init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) { + self.cuckoo_manager = manager + self.callMatcher = callMatcher + self.sourceLocation = sourceLocation + } + + + @discardableResult + func getRSSPMetadata() -> Cuckoo.__DoNotUse<(), RSSPMetadata> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify( + "getRSSPMetadata() async throws -> RSSPMetadata", + callMatcher: callMatcher, + parameterMatchers: matchers, + sourceLocation: sourceLocation + ) + } + + + @discardableResult + func getServiceAuthorizationUrl() -> Cuckoo.__DoNotUse<(), URL> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify( + "getServiceAuthorizationUrl() async throws -> URL", + callMatcher: callMatcher, + parameterMatchers: matchers, + sourceLocation: sourceLocation + ) + } + + + @discardableResult + func authorizeService(_ p0: M1) -> Cuckoo.__DoNotUse<(String), RQESServiceAuthorized> where M1.MatchedType == String { + let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: p0) { $0 }] + return cuckoo_manager.verify( + "authorizeService(_ p0: String) async throws -> RQESServiceAuthorized", + callMatcher: callMatcher, + parameterMatchers: matchers, + sourceLocation: sourceLocation + ) + } + + + @discardableResult + func authorizeCredential(_ p0: M1) -> Cuckoo.__DoNotUse<(String), RQESServiceCredentialAuthorized> where M1.MatchedType == String { + let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: p0) { $0 }] + return cuckoo_manager.verify( + "authorizeCredential(_ p0: String) async throws -> RQESServiceCredentialAuthorized", + callMatcher: callMatcher, + parameterMatchers: matchers, + sourceLocation: sourceLocation + ) + } + + + @discardableResult + func signDocuments(_ p0: M1) -> Cuckoo.__DoNotUse<(String), [Document]> where M1.MatchedType == String { + let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: p0) { $0 }] + return cuckoo_manager.verify( + "signDocuments(_ p0: String) async throws -> [Document]", + callMatcher: callMatcher, + parameterMatchers: matchers, + sourceLocation: sourceLocation + ) + } + + + @discardableResult + func getCredentialsList() -> Cuckoo.__DoNotUse<(), [CredentialInfo]> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify( + "getCredentialsList() async throws -> [CredentialInfo]", + callMatcher: callMatcher, + parameterMatchers: matchers, + sourceLocation: sourceLocation + ) + } + + + @discardableResult + func getCredentialAuthorizationUrl(credentialInfo p0: M1, documents p1: M2) -> Cuckoo.__DoNotUse<(CredentialInfo, [Document]), URL> where M1.MatchedType == CredentialInfo, M2.MatchedType == [Document] { + let matchers: [Cuckoo.ParameterMatcher<(CredentialInfo, [Document])>] = [wrap(matchable: p0) { $0.0 }, wrap(matchable: p1) { $0.1 }] + return cuckoo_manager.verify( + "getCredentialAuthorizationUrl(credentialInfo p0: CredentialInfo, documents p1: [Document]) async throws -> URL", + callMatcher: callMatcher, + parameterMatchers: matchers, + sourceLocation: sourceLocation + ) + } + } +} + +class RQESControllerStub:RQESController, @unchecked Sendable { + + + + func getRSSPMetadata() async throws -> RSSPMetadata { + return DefaultValueRegistry.defaultValue(for: (RSSPMetadata).self) + } + + func getServiceAuthorizationUrl() async throws -> URL { + return DefaultValueRegistry.defaultValue(for: (URL).self) + } + + func authorizeService(_ p0: String) async throws -> RQESServiceAuthorized { + return DefaultValueRegistry.defaultValue(for: (RQESServiceAuthorized).self) + } + + func authorizeCredential(_ p0: String) async throws -> RQESServiceCredentialAuthorized { + return DefaultValueRegistry.defaultValue(for: (RQESServiceCredentialAuthorized).self) + } + + func signDocuments(_ p0: String) async throws -> [Document] { + return DefaultValueRegistry.defaultValue(for: ([Document]).self) + } + + func getCredentialsList() async throws -> [CredentialInfo] { + return DefaultValueRegistry.defaultValue(for: ([CredentialInfo]).self) + } + + func getCredentialAuthorizationUrl(credentialInfo p0: CredentialInfo, documents p1: [Document]) async throws -> URL { + return DefaultValueRegistry.defaultValue(for: (URL).self) + } +} + + + + // MARK: - Mocks generated from file: 'Sources/Domain/DI/Graph/DIGraph.swift' import Cuckoo diff --git a/Tests/Util/TestConstants.swift b/Tests/Util/TestConstants.swift index 88e1726..2b474e7 100644 --- a/Tests/Util/TestConstants.swift +++ b/Tests/Util/TestConstants.swift @@ -14,6 +14,8 @@ * governing permissions and limitations under the Licence. */ @testable import EudiRQESUi +import RQES_LIBRARY +import RqesKit struct TestConstants { @@ -22,10 +24,111 @@ struct TestConstants { uri: URL(string: "file://internal/test.pdf")! ) + static let mockQtspData: QTSPData = .init( + name: "Wallet-Centric", + uri: URL(string: "https://walletcentric.signer.eudiw.dev/csc/v2")!, + scaURL: "https://walletcentric.signer.eudiw.dev" + ) + static let mockRqesService: RqesServiceConfig = .init( clientId: "wallet-client", clientSecret: "somesecret2", authFlowRedirectionURI: "rqes://oauth/callback", hashAlgorithm: .SHA256 ) + + static let mockSession: SessionData = .init(document: TestConstants.mockDocumentData) + + static func getMockAuthorizedService() async -> RQESServiceAuthorized { + let mockRQES: RQES = await .init( + cscClientConfig: mockcCClientConfig + ) + return .init( + mockRQES, + clientConfig: mockcCClientConfig, + defaultHashAlgorithmOID: .SHA256, + defaultSigningAlgorithmOID: .RSA, + fileExtension: ".pdf", + state: "state", + accessToken: "access_token" + ) + } + + static func getCredentialInfo() async throws -> CredentialInfo { + return try JSONDecoder().decode(CredentialInfo.self, from: Data(mockCredentialJson.utf8)) + } + + static func getMetaData() async throws -> RSSPMetadata { + return try JSONDecoder().decode(RSSPMetadata.self, from: Data(mockMetaData.utf8)) + } +} + +private extension TestConstants { + static let mockcCClientConfig: CSCClientConfig = .init( + OAuth2Client: .init( + clientId: "client_id", + clientSecret: "client_secret" + ), + authFlowRedirectionURI: "redirect", + scaBaseURL: "sca" + ) + + static let mockCredentialJson = + """ +{ + "credentialID":"id", + "key":{ + "status":"status", + "algo":[ + "TEST", + "TEST", + "TEST" + ], + "len":1 + }, + "cert":{ + "status":"status", + "issuerDN":"issuerDN", + "serialNumber":"serialNumber", + "subjectDN":"subjectDN", + "validFrom":"validFrom", + "validTo":"validTo", + "certificates":[ + "TEST", + "TEST", + "TEST" + ], + "len":1 + } +} +""" + + static let mockMetaData = + """ +{ + "specs": "Specification details", + "name": "Service Name", + "logo": "https://example.com/logo.png", + "region": "Europe", + "lang": "en", + "description": "This is a detailed description of the service.", + "authType": ["OAuth2", "APIKey"], + "oauth2": "https://example.com/oauth2", + "methods": ["GET", "POST", "PUT"], + "validationInfo": true, + "signAlgorithms": { + "algos": ["RS256", "ES256"], + "algoParams": ["param1", "param2"] + }, + "signature_formats": { + "formats": ["XML", "JSON"], + "envelope_properties": [ + ["property1", "property2"], + ["property3", "property4"] + ] + }, + "conformance_levels": ["Basic", "Advanced"] +} + +""" } diff --git a/fastlane/.xcovignore b/fastlane/.xcovignore index 739ce7a..eb5fc8e 100644 --- a/fastlane/.xcovignore +++ b/fastlane/.xcovignore @@ -23,6 +23,7 @@ # Controller - LogController.swift - PreferencesController.swift +- RQESController.swift # Extensions - .*Extensions.swift \ No newline at end of file