From edb3f822157715d5bd583d76bee1468d77836191 Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Fri, 19 Sep 2025 17:08:27 -0700 Subject: [PATCH 1/2] Fix failure with share functionality --- .../ImageOpening/Sources/ImageOpener.swift | 12 ++-- .../ImageOpening/Sources/SecureResource.swift | 18 +++++ .../ImageOpening/Tests/ImageOpenerTests.swift | 66 ++++++++++++++++--- .../Tests/SpySecureResource.swift | 46 +++++++++++++ .../Tests/SpySecureResourceTests.swift | 53 +++++++++++++++ 5 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 Modules/Capabilities/ImageOpening/Sources/SecureResource.swift create mode 100644 Modules/Capabilities/ImageOpening/Tests/SpySecureResource.swift create mode 100644 Modules/Capabilities/ImageOpening/Tests/SpySecureResourceTests.swift diff --git a/Modules/Capabilities/ImageOpening/Sources/ImageOpener.swift b/Modules/Capabilities/ImageOpening/Sources/ImageOpener.swift index a42e0566..1adad565 100644 --- a/Modules/Capabilities/ImageOpening/Sources/ImageOpener.swift +++ b/Modules/Capabilities/ImageOpening/Sources/ImageOpener.swift @@ -7,12 +7,14 @@ public struct ImageOpener { public init() {} public func openImage(at url: URL) throws -> UIImage { - guard url.startAccessingSecurityScopedResource() else { - throw ImageOpeningError.securityScopeDenied - } - defer { url.stopAccessingSecurityScopedResource() } + return try openImage(resource: url) + } + + func openImage(resource: SecureResource) throws -> UIImage { + _ = resource.startAccessingSecurityScopedResource() + defer { resource.stopAccessingSecurityScopedResource() } - let imageData = try Data(contentsOf: url) + let imageData = try resource.data guard let image = UIImage(data: imageData) else { throw ImageOpeningError.noImageFound } diff --git a/Modules/Capabilities/ImageOpening/Sources/SecureResource.swift b/Modules/Capabilities/ImageOpening/Sources/SecureResource.swift new file mode 100644 index 00000000..df70ecea --- /dev/null +++ b/Modules/Capabilities/ImageOpening/Sources/SecureResource.swift @@ -0,0 +1,18 @@ +// Created by Geoff Pado on 9/19/25. +// Copyright © 2025 Cocoatype, LLC. All rights reserved. + +import Foundation + +protocol SecureResource { + func startAccessingSecurityScopedResource() -> Bool + func stopAccessingSecurityScopedResource() + var data: Data { get throws } +} + +extension URL: SecureResource { + var data: Data { + get throws { + try Data(contentsOf: self) + } + } +} diff --git a/Modules/Capabilities/ImageOpening/Tests/ImageOpenerTests.swift b/Modules/Capabilities/ImageOpening/Tests/ImageOpenerTests.swift index 3c93bf13..69c1e1ad 100644 --- a/Modules/Capabilities/ImageOpening/Tests/ImageOpenerTests.swift +++ b/Modules/Capabilities/ImageOpening/Tests/ImageOpenerTests.swift @@ -8,15 +8,65 @@ import UIKit @testable import ImageOpening struct ImageOpenerTests { + static var imageData: Data { + get throws { + let image = try #require(UIImage(systemName: "bolt")) + return try #require(image.pngData()) + } + } + + @Test + func `openImage succeeds if not secure`() throws { + let resource = try SpySecureResource( + dataResult: .success(Self.imageData), + isSecure: false + ) + _ = try ImageOpener().openImage(resource: resource) + #expect(resource.startAccessingCalled) + #expect(resource.stopAccessingCalled) + } + + @Test + func `openImage fails if data errors and not secure`() throws { + let resource = SpySecureResource( + dataResult: .failure(TestError.sample), + isSecure: false + ) + + #expect(throws: TestError.sample) { + _ = try ImageOpener().openImage(resource: resource) + } + #expect(resource.startAccessingCalled) + #expect(resource.stopAccessingCalled) + } + @Test - func openImage() throws { - let boltImage = try #require(UIImage(systemName: "bolt")) - let boltData = try #require(boltImage.pngData()) - let boltEncoded = boltData.base64EncodedString() - let boltURL = try #require(URL(string: "data:image/png;base64,\(boltEncoded)")) - - #expect(throws: ImageOpeningError.securityScopeDenied) { - try ImageOpener().openImage(at: boltURL) + func `openImage succeeds if secure`() throws { + let resource = try SpySecureResource( + dataResult: .success(Self.imageData), + isSecure: true + ) + + _ = try ImageOpener().openImage(resource: resource) + #expect(resource.startAccessingCalled) + #expect(resource.stopAccessingCalled) + } + + @Test + func `openImage fails if data errors and secure`() throws { + let resource = SpySecureResource( + dataResult: .failure(TestError.sample), + isSecure: true + ) + + #expect(throws: TestError.sample) { + _ = try ImageOpener().openImage(resource: resource) } + #expect(resource.startAccessingCalled) + #expect(resource.stopAccessingCalled) + } + + enum TestError: Error { + case sample } } diff --git a/Modules/Capabilities/ImageOpening/Tests/SpySecureResource.swift b/Modules/Capabilities/ImageOpening/Tests/SpySecureResource.swift new file mode 100644 index 00000000..3afc01f3 --- /dev/null +++ b/Modules/Capabilities/ImageOpening/Tests/SpySecureResource.swift @@ -0,0 +1,46 @@ +// Created by Geoff Pado on 9/19/25. +// Copyright © 2025 Cocoatype, LLC. All rights reserved. + +import Foundation + +@testable import ImageOpening + +class SpySecureResource: SecureResource { + private let dataResult: Result + private let isSecure: Bool + + init( + dataResult: Result, + isSecure: Bool, + ) { + self.dataResult = dataResult + self.isSecure = isSecure + } + + private(set) var startAccessingCalled = false + func startAccessingSecurityScopedResource() -> Bool { + startAccessingCalled = true + return isSecure + } + + private(set) var stopAccessingCalled = false + func stopAccessingSecurityScopedResource() { + stopAccessingCalled = true + } + + var data: Data { + get throws { + guard isSecure else { return try dataResult.get() } + + guard startAccessingCalled else { throw Error.startNotCalled } + guard stopAccessingCalled == false else { throw Error.stopAlreadyCalled } + + return try dataResult.get() + } + } + + enum Error: Swift.Error { + case startNotCalled + case stopAlreadyCalled + } +} diff --git a/Modules/Capabilities/ImageOpening/Tests/SpySecureResourceTests.swift b/Modules/Capabilities/ImageOpening/Tests/SpySecureResourceTests.swift new file mode 100644 index 00000000..da75b49c --- /dev/null +++ b/Modules/Capabilities/ImageOpening/Tests/SpySecureResourceTests.swift @@ -0,0 +1,53 @@ +// Created by Geoff Pado on 9/19/25. +// Copyright © 2025 Cocoatype, LLC. All rights reserved. + +import Testing + +struct SpySecureResourceTests { + @Test func `data fails if start not called`() throws { + let resource = SpySecureResource( + dataResult: .failure(TestError.sample), + isSecure: true + ) + + #expect(throws: SpySecureResource.Error.startNotCalled) { + _ = try resource.data + } + } + + @Test func `data fails if stop called early`() throws { + let resource = SpySecureResource( + dataResult: .failure(TestError.sample), + isSecure: true + ) + + _ = resource.startAccessingSecurityScopedResource() + resource.stopAccessingSecurityScopedResource() + + #expect(throws: SpySecureResource.Error.stopAlreadyCalled) { + _ = try resource.data + } + } + + @Test func `startAccessing returns true if secure`() throws { + let resource = SpySecureResource( + dataResult: .failure(TestError.sample), + isSecure: true + ) + + #expect(resource.startAccessingSecurityScopedResource() == true) + } + + @Test func `startAccessing returns false if not secure`() throws { + let resource = SpySecureResource( + dataResult: .failure(TestError.sample), + isSecure: false + ) + + #expect(resource.startAccessingSecurityScopedResource() == false) + } + + enum TestError: Error { + case sample + } +} From 6f67a8241cb15c0433b234741ac493f0a90c571e Mon Sep 17 00:00:00 2001 From: Geoff Pado Date: Fri, 19 Sep 2025 17:20:50 -0700 Subject: [PATCH 2/2] Fix linting error --- Modules/Capabilities/ImageOpening/Tests/SpySecureResource.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Capabilities/ImageOpening/Tests/SpySecureResource.swift b/Modules/Capabilities/ImageOpening/Tests/SpySecureResource.swift index 3afc01f3..d7678e13 100644 --- a/Modules/Capabilities/ImageOpening/Tests/SpySecureResource.swift +++ b/Modules/Capabilities/ImageOpening/Tests/SpySecureResource.swift @@ -27,7 +27,7 @@ class SpySecureResource: SecureResource { func stopAccessingSecurityScopedResource() { stopAccessingCalled = true } - + var data: Data { get throws { guard isSecure else { return try dataResult.get() }