From 92f9ffef0f4b9f3c45fb3418f1725a78dd5bc613 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Mon, 31 Aug 2026 14:42:19 +0300 Subject: [PATCH] Hide Encrypt button for DDOC and ASiC-S containers --- .../Sources/CommonsLib/Constants.swift | 2 + .../Domain/Service/Siva/SivaService.swift | 5 +- .../Container/Signing/SigningView.swift | 18 +- .../Protocols/SigningViewModelProtocol.swift | 3 +- RIADigiDoc/ViewModel/SigningViewModel.swift | 33 +-- .../Service/Siva/SivaServiceTests.swift | 49 ++++ .../ViewModel/SigningViewModelTests.swift | 212 ++++++++++++++---- 7 files changed, 249 insertions(+), 73 deletions(-) diff --git a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift index e6ea5d41..2acc3a54 100644 --- a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift +++ b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift @@ -49,6 +49,7 @@ public struct Constants { public static let SignatureContainers = [Asice, Asics, Ddoc, Bdoc, Adoc] public static let SivaContainers = [Ddoc, Asics] public static let UnsignableContainers = [Adoc, Ddoc, Asics] + public static let UnencryptableContainers = [Ddoc, Asics] public static let Pdf = "application/pdf" @@ -77,6 +78,7 @@ public struct Constants { public static let CryptoContainers = [Cdoc, Cdoc2] public static let UnsignableContainerExtensions = [Adoc, Ddoc] + AsicsContainers + public static let UnencryptableContainerExtensions = [Ddoc] + AsicsContainers } public struct Identifier { diff --git a/RIADigiDoc/Domain/Service/Siva/SivaService.swift b/RIADigiDoc/Domain/Service/Siva/SivaService.swift index 73a99b8e..d0908570 100644 --- a/RIADigiDoc/Domain/Service/Siva/SivaService.swift +++ b/RIADigiDoc/Domain/Service/Siva/SivaService.swift @@ -49,7 +49,7 @@ actor SivaService: SivaServiceProtocol { guard let file = files.first else { return false } - let mimetype = await mimeTypeResolver.mimeType(url: file) + let mimetype = await mimeTypeResolver.mimeType(url: file).lowercased() let isCades = await file.isCades(fileUtil: fileUtil) let isXades = await file.isXades(fileUtil: fileUtil) @@ -65,7 +65,8 @@ actor SivaService: SivaServiceProtocol { func isTimestampedContainer(signedContainer: SignedContainerProtocol) async -> Bool { let isOneDataFileInContainer = await signedContainer.getDataFiles().count == 1 - let isAsicsMimeType = await signedContainer.getContainerMimetype() == Constants.MimeType.Asics + let isAsicsMimeType = await signedContainer.getContainerMimetype() + .lowercased() == Constants.MimeType.Asics let isTimeStampTokenSignatureMethod = await signedContainer.getSignatures().first?.format == "TimeStampToken" return isOneDataFileInContainer && diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index db75f08d..7ece0b40 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -53,7 +53,6 @@ struct SigningView: View { @State private var showingShareSheet = false @State private var isSignButtonShown = false - @State private var isEncryptButtonShown = false @State private var selectedDataFile: DataFileWrapper? @State private var showSivaMessage = false @@ -208,10 +207,10 @@ struct SigningView: View { isEditContainerButtonShown: !isContainerSigned && !isNestedContainer, isSaveButtonShown: true, isSignButtonShown: false, - isEncryptButtonShown: !isContainerSigned && !isNestedContainer, + isEncryptButtonShown: !isContainerSigned && viewModel.canEncrypt, isExtendSignaturesButtonShown: isExtendSignaturesButtonShown, showLeftActionButton: isContainerSigned && isSignButtonShown, - showRightActionButton: isContainerSigned && !isNestedContainer, + showRightActionButton: isContainerSigned && viewModel.canEncrypt, leftActionButtonName: languageSettings.localized("Add signature"), rightActionButtonName: encryptLabel, leftActionButtonAccessibilityLabel: signAccessibilityLabel.lowercased(), @@ -267,7 +266,7 @@ struct SigningView: View { ) .onChange(of: viewModel.isNestedContainer()) { Task { - await updateSignAndEncryptButtonVisibility() + await updateSignButtonVisibility() } } @@ -419,7 +418,7 @@ struct SigningView: View { signedContainer: viewModel.signedContainer ) - await updateSignAndEncryptButtonVisibility() + await updateSignButtonVisibility() } } .onDisappear { @@ -565,22 +564,15 @@ struct SigningView: View { } } - private func updateSignAndEncryptButtonVisibility() async { + private func updateSignButtonVisibility() async { let shouldShowSignButton = await viewModel .isSignButtonShown( signedContainer: viewModel.signedContainer, isNestedContainer: isNestedContainer ) - let shouldShowEncryptButton = await viewModel - .isEncryptButtonShown( - signedContainer: viewModel.signedContainer, - isNestedContainer: isNestedContainer - ) - await MainActor.run { isSignButtonShown = shouldShowSignButton - isEncryptButtonShown = shouldShowEncryptButton } } diff --git a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift index 06182994..6f81aeab 100644 --- a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift @@ -34,8 +34,9 @@ public protocol SigningViewModelProtocol: Sendable { func isNestedContainer() -> Bool func isSivaConfirmationNeeded(dataFile: DataFileWrapper) async -> Bool func isSignButtonShown(signedContainer: SignedContainerProtocol?, isNestedContainer: Bool) async -> Bool - func isEncryptButtonShown(signedContainer: SignedContainerProtocol?, isNestedContainer: Bool) async -> Bool func isSignatureRemoveButtonShown() -> Bool + var isEncryptableContainer: Bool { get } + var canEncrypt: Bool { get } func isTimestampedContainer() async -> Bool func getContainerNotifications(container: SignedContainerProtocol) async -> [ContainerNotificationType] func removeSignature(_ signature: SignatureWrapper) async diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index c8d49594..9b4f2bc2 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -103,16 +103,18 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { self.signedContainer = openedContainer - self.containerName = await openedContainer.getContainerName() + let name = await openedContainer.getContainerName() + let mimetype = await openedContainer.getContainerMimetype() + + self.containerName = name self.dataFiles = await openedContainer.getDataFiles() self.signatures = await openedContainer.getSignatures() self.timestamps = await openedContainer.getTimestamps() - self.containerMimetype = await openedContainer.getContainerMimetype() + self.containerMimetype = mimetype self.containerURL = await openedContainer.getRawContainerFile() self.isTimestampedContainer = await isTimestampedContainer() self.isCadesContainer = await openedContainer.isCades() self.isXadesContainer = await openedContainer.isXades() - self.containerNotifications = await getContainerNotifications(container: openedContainer) SigningViewModel.logger().info("Signed container data loaded") @@ -466,6 +468,17 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { } } + var isEncryptableContainer: Bool { + guard signedContainer != nil else { return false } + return !Constants.MimeType.UnencryptableContainers.contains(containerMimetype.lowercased()) && + !Constants.Extension.UnencryptableContainerExtensions + .contains((containerName as NSString).pathExtension.lowercased()) + } + + var canEncrypt: Bool { + !isNestedContainer() && isEncryptableContainer + } + func isNestedContainer() -> Bool { return sharedContainerViewModel.isNestedContainer( sharedContainerViewModel.currentContainer() @@ -498,20 +511,12 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { let isEmptyFileInContainer = await signedContainer?.isEmptyFileInContainer() ?? false return signedContainer != nil && - (!Constants.MimeType.UnsignableContainers.contains(mimetype)) && - (!Constants.Extension.UnsignableContainerExtensions.contains((name as NSString).pathExtension)) && + (!Constants.MimeType.UnsignableContainers.contains(mimetype.lowercased())) && + (!Constants.Extension.UnsignableContainerExtensions + .contains((name as NSString).pathExtension.lowercased())) && !isNestedContainer && !isEmptyFileInContainer && !isCadesContainer && !isXadesContainer } - func isEncryptButtonShown( - signedContainer: SignedContainerProtocol?, - isNestedContainer: Bool, - ) async -> Bool { - guard let container = signedContainer else { return false } - let isExistingContainer = await container.isExistingContainer() - return (isExistingContainer || isSigned()) && !isNestedContainer - } - func isSignatureRemoveButtonShown() -> Bool { return !isNestedContainer() && !isCadesContainer && !isXadesContainer } diff --git a/RIADigiDocTests/Domain/Service/Siva/SivaServiceTests.swift b/RIADigiDocTests/Domain/Service/Siva/SivaServiceTests.swift index da8bb91c..40ddfa0c 100644 --- a/RIADigiDocTests/Domain/Service/Siva/SivaServiceTests.swift +++ b/RIADigiDocTests/Domain/Service/Siva/SivaServiceTests.swift @@ -84,6 +84,39 @@ struct SivaServiceTests { #expect(isSivaConfirmationNeeded) } + @Test + func isSivaConfirmationNeeded_returnTrueForUppercaseAsicsMimetypeWrappingDdoc() async throws { + let wrappedDdoc = URL(fileURLWithPath: "/mock/path/wrapped.ddoc") + let mockContainer = try TestContainerUtil.createMockContainer( + with: [wrappedDdoc.lastPathComponent: "Test content"], + containerExtension: "asics") + defer { try? FileManager.default.removeItem(at: mockContainer) } + + mockMimetypeResolver.mimeTypeHandler = { _ in Constants.MimeType.Asics.uppercased() } + mockFileUtil.getFileFromZipFileHandler = { _, name in + name.contains(".ddoc") ? wrappedDdoc : nil + } + + let isSivaConfirmationNeeded = await service.isSivaConfirmationNeeded(files: [mockContainer]) + + #expect(isSivaConfirmationNeeded) + } + + @Test + func isSivaConfirmationNeeded_returnTrueForUppercaseDdocMimetype() async throws { + let mockContainer = try TestContainerUtil.createMockContainer( + with: ["content.txt": "Test content"], + containerExtension: "ddoc") + defer { try? FileManager.default.removeItem(at: mockContainer) } + + mockMimetypeResolver.mimeTypeHandler = { _ in Constants.MimeType.Ddoc.uppercased() } + mockFileUtil.getFileFromZipFileHandler = { _, _ in nil } + + let isSivaConfirmationNeeded = await service.isSivaConfirmationNeeded(files: [mockContainer]) + + #expect(isSivaConfirmationNeeded) + } + @Test func isSivaConfirmationNeeded_returnFalseForAsicsWrappingBdoc() async throws { let mockContainer = try TestContainerUtil.createMockContainer( @@ -130,6 +163,22 @@ struct SivaServiceTests { #expect(mockSignedContainer.getSignaturesCallCount == 1) } + @Test + func isTimestampedContainer_returnTrueWithUppercaseAsicsMimetype() async { + let mockSignedContainer = SignedContainerProtocolMock() + mockSignedContainer.getDataFilesHandler = {[ + MockDataFileWrapper.mockDataFileWrapper() + ]} + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asics.uppercased() } + mockSignedContainer.getSignaturesHandler = {[ + MockSignatureWrapper.mockSignatureWrapper(format: "TimeStampToken") + ]} + + let isTimestampedContainer = await service.isTimestampedContainer(signedContainer: mockSignedContainer) + + #expect(isTimestampedContainer) + } + @Test func isTimestampedContainer_returnFalseWithMultipleDataFiles() async { let mockSignedContainer = SignedContainerProtocolMock() diff --git a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift index c20645f3..f30bd39f 100644 --- a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift @@ -826,6 +826,20 @@ struct SigningViewModelTests: Loggable { #expect(!isSignButtonShown) } + @Test + func isSignButtonShown_returnFalseWithUppercaseUnsignableExtension() async { + let container = SignedContainerProtocolMock() + container.getContainerMimetypeHandler = { Constants.MimeType.Container } + container.getContainerNameHandler = { "mockContainer.ASICS" } + + let isSignButtonShown = await viewModel.isSignButtonShown( + signedContainer: container, + isNestedContainer: false + ) + + #expect(!isSignButtonShown) + } + @Test func isSignButtonShown_returnFalseWithNestedContainer() async { let container = SignedContainerProtocolMock() @@ -851,92 +865,204 @@ struct SigningViewModelTests: Loggable { } @Test - func isEncryptButtonShown_returnTrueWithExistingContainer() async { + func isEncryptableContainer_returnTrueWithAsiceContainer() async { let mockSignedContainer = SignedContainerProtocolMock() - mockSignedContainer.isExistingContainerHandler = { true } mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.getContainerNameHandler = { "mockContainer.asice" } await viewModel.loadContainerData(signedContainer: mockSignedContainer) - let isEncryptButtonShown = await viewModel.isEncryptButtonShown( - signedContainer: mockSignedContainer, - isNestedContainer: false - ) + #expect(viewModel.isEncryptableContainer) + } - #expect(isEncryptButtonShown) + @Test + func isEncryptableContainer_returnTrueWithAdocContainer() async { + let mockSignedContainer = SignedContainerProtocolMock() + + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Adoc } + mockSignedContainer.getContainerNameHandler = { "mockContainer.adoc" } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + + #expect(viewModel.isEncryptableContainer) } @Test - func isEncryptButtonShown_returnTrueWithSignedContainer() async { + func isEncryptableContainer_returnFalseWithDdocContainer() async { let mockSignedContainer = SignedContainerProtocolMock() - mockSignedContainer.isExistingContainerHandler = { false } - mockSignedContainer.getSignaturesHandler = { - [MockSignatureWrapper.mockSignatureWrapper()] - } + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Ddoc } + mockSignedContainer.getContainerNameHandler = { "mockContainer.ddoc" } await viewModel.loadContainerData(signedContainer: mockSignedContainer) - let isEncryptButtonShown = await viewModel.isEncryptButtonShown( - signedContainer: mockSignedContainer, - isNestedContainer: false - ) + #expect(!viewModel.isEncryptableContainer) + } + + @Test + func isEncryptableContainer_returnFalseWithAsicsContainer() async { + let mockSignedContainer = SignedContainerProtocolMock() - #expect(isEncryptButtonShown) + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asics } + mockSignedContainer.getContainerNameHandler = { "mockContainer.asics" } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + + #expect(!viewModel.isEncryptableContainer) } @Test - func isEncryptButtonShown_notSignedOrExisting_returnFalseWhenContainerIsNotSignedOrExisting() async { + func isEncryptableContainer_returnFalseWithScsContainer() async { let mockSignedContainer = SignedContainerProtocolMock() - mockSignedContainer.isExistingContainerHandler = { false } + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Container } + mockSignedContainer.getContainerNameHandler = { "mockContainer.scs" } await viewModel.loadContainerData(signedContainer: mockSignedContainer) - let isEncryptButtonShown = await viewModel.isEncryptButtonShown( - signedContainer: mockSignedContainer, - isNestedContainer: false - ) + #expect(!viewModel.isEncryptableContainer) + } - #expect(!isEncryptButtonShown) + @Test + func isEncryptableContainer_returnFalseWithUppercaseDdocExtension() async { + let mockSignedContainer = SignedContainerProtocolMock() + + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Container } + mockSignedContainer.getContainerNameHandler = { "mockContainer.DDOC" } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + + #expect(!viewModel.isEncryptableContainer) } @Test - func isEncryptButtonShown_returnFalseWithNestedContainer() async { + func isEncryptableContainer_returnTrueWithBdocContainer() async { let mockSignedContainer = SignedContainerProtocolMock() - mockSignedContainer.isExistingContainerHandler = { true } - mockSignedContainer.getSignaturesHandler = { - [MockSignatureWrapper.mockSignatureWrapper()] - } + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Bdoc } + mockSignedContainer.getContainerNameHandler = { "mockContainer.bdoc" } await viewModel.loadContainerData(signedContainer: mockSignedContainer) - let isEncryptButtonShown = await viewModel.isEncryptButtonShown( - signedContainer: mockSignedContainer, - isNestedContainer: true - ) + #expect(viewModel.isEncryptableContainer) + } - #expect(!isEncryptButtonShown) + @Test + func isEncryptableContainer_returnFalseWhenOnlyMimetypeIsUnencryptable() async { + let mockSignedContainer = SignedContainerProtocolMock() + + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asics } + mockSignedContainer.getContainerNameHandler = { "mockContainer.asice" } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + + #expect(!viewModel.isEncryptableContainer) } @Test - func isEncryptButtonShown_returnFalseIfContainerNil() async { + func isEncryptableContainer_returnFalseForTimestampedAsicsReportingInnerMimetype() async { let mockSignedContainer = SignedContainerProtocolMock() - mockSignedContainer.getSignaturesHandler = { - [MockSignatureWrapper.mockSignatureWrapper()] - } + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.getContainerNameHandler = { "mockContainer.asics" } await viewModel.loadContainerData(signedContainer: mockSignedContainer) - let isEncryptButtonShown = await viewModel.isEncryptButtonShown( - signedContainer: nil, - isNestedContainer: false - ) + #expect(!viewModel.isEncryptableContainer) + } + + @Test + func isEncryptableContainer_returnFalseBeforeContainerIsLoaded() { + #expect(!viewModel.isEncryptableContainer) + } + + @Test + func isEncryptableContainer_returnFalseWithUppercaseUnencryptableMimetype() async { + let mockSignedContainer = SignedContainerProtocolMock() + + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Ddoc.uppercased() } + mockSignedContainer.getContainerNameHandler = { "mockContainer" } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + + #expect(!viewModel.isEncryptableContainer) + } + + @Test + func isEncryptableContainer_reflectsContainerNameChangedAfterLoad() async { + let mockSignedContainer = SignedContainerProtocolMock() + + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.getContainerNameHandler = { "mockContainer.asice" } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + #expect(viewModel.isEncryptableContainer) + + viewModel.containerName = "mockContainer.ddoc" + + #expect(!viewModel.isEncryptableContainer) + } + + @Test + func canEncrypt_returnFalseWithNestedContainer() async { + let mockSignedContainer = SignedContainerProtocolMock() + + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.getContainerNameHandler = { "mockContainer.asice" } + mockSharedContainerViewModel.isNestedContainerHandler = { _ in true } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + + #expect(viewModel.isEncryptableContainer) + #expect(!viewModel.canEncrypt) + } + + @Test + func canEncrypt_returnTrueWithNonNestedEncryptableContainer() async { + let mockSignedContainer = SignedContainerProtocolMock() + + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.getContainerNameHandler = { "mockContainer.asice" } + mockSharedContainerViewModel.isNestedContainerHandler = { _ in false } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + + #expect(viewModel.canEncrypt) + } + + @Test + func canEncrypt_returnFalseWithUnencryptableNonNestedContainer() async { + let mockSignedContainer = SignedContainerProtocolMock() + + mockSignedContainer.getSignaturesHandler = { [] } + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Ddoc } + mockSignedContainer.getContainerNameHandler = { "mockContainer.ddoc" } + mockSharedContainerViewModel.isNestedContainerHandler = { _ in false } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + + #expect(!viewModel.canEncrypt) + } + + @Test + func canEncrypt_returnFalseBeforeContainerIsLoaded() { + mockSharedContainerViewModel.isNestedContainerHandler = { _ in false } - #expect(!isEncryptButtonShown) + #expect(!viewModel.canEncrypt) } @Test