diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt index fee46d45b..6a8bd1858 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt @@ -113,8 +113,10 @@ class GutenbergView : FrameLayout { var requestInterceptor: GutenbergRequestInterceptor = DefaultGutenbergRequestInterceptor() /** - * Optional delegate for customizing media upload behavior (resize, transcode, - * custom upload). + * Optional delegate for transforming media before upload (resize, transcode, + * strip EXIF). + * + * To perform the upload yourself, set [mediaUploader] instead. * * Provide this **before the editor loads** — typically right after * construction (e.g. in the `AndroidView` factory). It is captured once, when @@ -136,8 +138,8 @@ class GutenbergView : FrameLayout { * and this view owns it for its lifetime — so you needn't retain it yourself, just * don't strongly retain this [GutenbergView] from your uploader. * - * Takes precedence over the deprecated [MediaUploadDelegate.uploadFile]: with an - * uploader set, that hook is never called. + * A [mediaUploadDelegate] can still transform the file first; only delivery moves + * to the uploader. */ var mediaUploader: MediaUploader? = null set(value) { diff --git a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt index 27c58f2f4..abe448e51 100644 --- a/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt +++ b/android/Gutenberg/src/main/java/org/wordpress/gutenberg/MediaUploadServer.kt @@ -33,8 +33,11 @@ import okio.source * so every consumer — image sub-sizes, attachment links, error notices — * behaves identically to a non-native upload. */ -class MediaUploadResponse( - /** The HTTP status code WordPress (or the host's upload service) returned. */ +internal class MediaUploadResponse( + /** + * The HTTP status code WordPress returned, or 201 for an upload a + * [MediaUploader] delivered. + */ val statusCode: Int, /** * The raw response body — a WordPress REST attachment on success, or a @@ -70,23 +73,30 @@ sealed class ProcessedProxyFile { } /** - * Interface for customizing media upload behavior. + * Transforms media before GutenbergKit delivers it. + * + * A delegate only changes *bytes* — GutenbergKit still uploads the result to the + * configured site and owns the whole lifecycle (retries, cleanup). Because it never + * performs the upload itself, it cannot deliver media to the wrong place. Set + * [GutenbergView.mediaUploadDelegate] to resize images, transcode video, strip EXIF, + * etc. * - * The native host app can provide an implementation to resize images, - * transcode video, or use its own upload service. + * This is the safe, common extension point: most hosts want only this. To perform the + * upload yourself, implement [MediaUploader] instead. */ interface MediaUploadDelegate { /** - * Whether this delegate might handle a file with the given metadata — either - * processing it ([processFile]) or uploading it itself ([uploadFile]). + * Whether this delegate might transform a file with the given metadata. * * A cheap, metadata-only gate the server consults *before* materializing the * upload to a temp file. Return false to decline a file by type — e.g. an * image-only delegate returning false for a video — so the server forwards * the original upload to WordPress without first copying a file the delegate - * won't touch. Because it gates the temp-file copy needed by *both* - * [processFile] and [uploadFile], return true for any file the delegate will - * either process or upload itself. + * won't touch. + * + * With a [MediaUploader] set this can't decline the upload itself — an uploader + * delivers every file, so there is no passthrough to fall to — but it still gates + * [processFile]: a declined file reaches the uploader unprocessed. * * Defaults to true: every file is materialized and the full pipeline runs. A * true here is not a commitment — [processFile] may still return @@ -103,30 +113,6 @@ interface MediaUploadDelegate { * stores it with the correct extension and type. */ suspend fun processFile(file: File, mimeType: String, filename: String): ProcessedProxyFile = ProcessedProxyFile.Original - - /** - * Upload a processed file to the remote WordPress site. - * - * Return the raw WordPress response (status code + body), which GutenbergKit - * relays to the editor unchanged, or null to use the internal media client. A - * host that uploads to WordPress should return the exact response it received so - * the editor sees a complete attachment object. - * - * Returning a raw response splits one upload's HTTP across two owners: you - * perform the POST, but the editor drives the `post-process` retries and orphan - * cleanup behind it, through the WebView rather than your stack. It also receives - * no form fields, so an attachment uploaded this way lands unattached to its post. - * Implement [MediaUploader] instead — it owns the upload end-to-end and receives a - * [MediaUpload] carrying the fields. - */ - // No ReplaceWith: it takes a replacement *expression* the IDE substitutes for the - // call, and there is none that means "implement a different interface" — the - // quick-fix would drop the arguments and leave a type name where a - // MediaUploadResponse? was expected. The message carries the guidance instead. - @Deprecated( - "Implement MediaUploader instead — it owns the upload's retries and receives the editor's form fields." - ) - suspend fun uploadFile(file: File, mimeType: String, filename: String): MediaUploadResponse? = null } /** @@ -380,8 +366,8 @@ internal class MediaUploadServer( // Ask the delegate — from metadata alone — whether it will touch a file // like this. If not, forward the original upload to WordPress directly, - // skipping a full temp-file copy of a file the delegate won't process or - // upload (e.g. a video handed to an image-only delegate). + // skipping a full temp-file copy of a file the delegate won't process + // (e.g. a video handed to an image-only delegate). // An uploader takes over delivery for *every* file, so with one set there is no // passthrough to fall to and the gate can't decline the upload outright. It // still decides whether processFile runs, though — a declined file is handed to @@ -594,13 +580,6 @@ internal class MediaUploadServer( return UploadResult.Uploaded(MediaUploadResponse(201, hostUploader.upload(upload))) } - // The deprecated delegate path: the host performs the POST but returns the - // raw response, leaving the editor to drive post-process recovery behind it. - @Suppress("DEPRECATION") - uploadDelegate?.uploadFile(targetFile, targetMimeType, targetFilename)?.let { - return UploadResult.Uploaded(it) - } - // Unmodified — forward the original request body directly, skipping // multipart re-encoding. if (processed is ProcessedProxyFile.Original) { @@ -608,7 +587,7 @@ internal class MediaUploadServer( } val result = internalClient?.upload(targetFile, targetMimeType, targetFilename, extraParts, query) - ?: error("No upload delegate or internal media client configured") + ?: error("No media uploader or internal media client configured") return UploadResult.Uploaded(result) } finally { // The processed file (if the delegate produced a new one) is ours to diff --git a/android/Gutenberg/src/test/java/org/wordpress/gutenberg/MediaUploadServerTest.kt b/android/Gutenberg/src/test/java/org/wordpress/gutenberg/MediaUploadServerTest.kt index 3e43d0902..a842c2bf4 100644 --- a/android/Gutenberg/src/test/java/org/wordpress/gutenberg/MediaUploadServerTest.kt +++ b/android/Gutenberg/src/test/java/org/wordpress/gutenberg/MediaUploadServerTest.kt @@ -245,11 +245,11 @@ class MediaUploadServerTest { } @Test - fun `an uploader takes precedence over the deprecated uploadFile hook`() { - // Both set: the uploader owns delivery and the deprecated hook must not run. - // The delegate still processes — only delivery moves to the uploader. + fun `a delegate still processes the file an uploader delivers`() { + // With both set, the delegate still processes — only delivery moves to + // the uploader. val uploader = RecordingUploader() - val delegate = MockUploadDelegate() + val delegate = ProcessOnlyDelegate() val client = MockInternalMediaClient() server.stop() server = MediaUploadServer( @@ -270,7 +270,6 @@ class MediaUploadServerTest { ) assertNotNull(uploader.received) - assertFalse(delegate.uploadFileCalled) assertTrue(delegate.processFileCalled) assertFalse(client.uploadCalled) } @@ -343,10 +342,11 @@ class MediaUploadServerTest { // MARK: - Upload with delegate @Test - fun `calls delegate processFile and uploadFile`() { - val delegate = MockUploadDelegate() + fun `processes with the delegate, then delivers through the internal client`() { + val delegate = TranscodingDelegate() + val client = MockInternalMediaClient() server.stop() - server = MediaUploadServer(uploadDelegate = delegate, internalClient = null, cacheDir = tempFolder.root) + server = MediaUploadServer(uploadDelegate = delegate, internalClient = client, cacheDir = tempFolder.root) val boundary = "test-boundary-123" val body = buildMultipartBody(boundary, "photo.jpg", "image/jpeg", "fake image data".toByteArray()) @@ -362,16 +362,14 @@ class MediaUploadServerTest { ) assertTrue("Expected 201 but got: ${response.statusLine}", response.statusLine.contains("201")) - assertTrue(delegate.processFileCalled) - assertTrue(delegate.uploadFileCalled) - assertEquals("image/jpeg", delegate.lastMimeType) - assertEquals("photo.jpg", delegate.lastFilename) + // The delegate only transforms; GutenbergKit performs the upload. + assertTrue(client.uploadCalled) // The server relays WordPress's raw response body verbatim. val json = JsonParser.parseString(response.body).asJsonObject - assertEquals(42, json.get("id").asInt) - assertEquals("https://example.com/photo.jpg", json.get("source_url").asString) - assertEquals("image", json.get("media_type").asString) + assertEquals(99, json.get("id").asInt) + assertEquals("https://example.com/doc.pdf", json.get("source_url").asString) + assertEquals("file", json.get("media_type").asString) } @Test @@ -896,27 +894,6 @@ class MediaUploadServerTest { // MARK: - Mocks - private class MockUploadDelegate : MediaUploadDelegate { - @Volatile var processFileCalled = false - @Volatile var uploadFileCalled = false - @Volatile var lastMimeType: String? = null - @Volatile var lastFilename: String? = null - - override suspend fun processFile(file: File, mimeType: String, filename: String): ProcessedProxyFile { - processFileCalled = true - lastMimeType = mimeType - return ProcessedProxyFile.Original - } - - @Suppress("OVERRIDE_DEPRECATION") - override suspend fun uploadFile(file: File, mimeType: String, filename: String): MediaUploadResponse? { - uploadFileCalled = true - lastFilename = filename - val json = """{"id":42,"source_url":"https://example.com/photo.jpg","media_type":"image"}""" - return MediaUploadResponse(201, json.toByteArray()) - } - } - private class ProcessOnlyDelegate : MediaUploadDelegate { @Volatile var processFileCalled = false diff --git a/android/app/src/main/java/com/example/gutenbergkit/DemoMediaUploadDelegate.kt b/android/app/src/main/java/com/example/gutenbergkit/DemoMediaUploadDelegate.kt index 572836e4c..dcad5644e 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/DemoMediaUploadDelegate.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/DemoMediaUploadDelegate.kt @@ -13,7 +13,7 @@ import java.io.IOException /** * Demo media upload delegate that resizes images to a maximum dimension of 2000px. * - * Only overrides [processFile] — [uploadFile] returns null so the default uploader is used. + * Only transforms the file; GutenbergKit performs the upload. */ class DemoMediaUploadDelegate : MediaUploadDelegate { companion object { diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index 4c05a72cc..c5a129e28 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -109,7 +109,9 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro /// take effect, so its setter traps if written. private var hasStartedLoading = false - /// Delegate for customizing media file processing and upload behavior. + /// Delegate for transforming media before upload — resize, transcode, strip EXIF. + /// + /// To perform the upload yourself, set ``mediaUploader`` instead. /// /// Provide this **before the editor loads** — typically right after `init`, the /// same way the rest of the editor configuration is supplied. It is captured @@ -147,8 +149,8 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro /// retain it yourself — just don't strongly retain this `EditorViewController` /// from your uploader. /// - /// Takes precedence over the deprecated ``MediaUploadDelegate/uploadFile(at:mimeType:filename:)``: - /// with an uploader set, that hook is never called. + /// A ``mediaUploadDelegate`` can still transform the file first; only delivery + /// moves to the uploader. public var mediaUploader: (any MediaUploader)? { didSet { precondition(!hasStartedLoading, Self.lateMediaAssignmentMessage("mediaUploader")) diff --git a/ios/Sources/GutenbergKit/Sources/Media/MediaUploadDelegate.swift b/ios/Sources/GutenbergKit/Sources/Media/MediaUploadDelegate.swift index a5bfb7d5b..f9aa87ec0 100644 --- a/ios/Sources/GutenbergKit/Sources/Media/MediaUploadDelegate.swift +++ b/ios/Sources/GutenbergKit/Sources/Media/MediaUploadDelegate.swift @@ -7,13 +7,14 @@ import Foundation /// or WordPress REST error object (on failure) it would get from a direct /// upload, so every consumer — image sub-sizes, attachment links, error notices — /// behaves identically to a non-native upload. -public struct MediaUploadResponse: Sendable { - /// The HTTP status code WordPress (or the host's upload service) returned. - public let statusCode: Int +struct MediaUploadResponse: Sendable { + /// The HTTP status code WordPress returned, or 201 for an upload a + /// ``MediaUploader`` delivered. + let statusCode: Int /// The raw response body — a WordPress REST attachment on success, or a /// WordPress REST error object (`{ "code", "message", "data" }`) on failure. - public let body: Data + let body: Data /// The response headers to relay to the editor. /// @@ -22,9 +23,9 @@ public struct MediaUploadResponse: Sendable { /// metadata generation fataled, and the editor's api-fetch middleware reads /// it to retry `post-process` and clean up the orphan. Dropping it turns a /// recoverable upload into a permanent failure. - public let headers: [String: String] + let headers: [String: String] - public init(statusCode: Int, body: Data, headers: [String: String] = [:]) { + init(statusCode: Int, body: Data, headers: [String: String] = [:]) { self.statusCode = statusCode self.body = body self.headers = headers @@ -44,23 +45,28 @@ public enum ProcessedProxyFile: Sendable { case processed(URL, mimeType: String, filename: String) } -/// Protocol for customizing media upload behavior. +/// Transforms media before GutenbergKit delivers it. /// -/// The native host app can provide an implementation to resize images, -/// transcode video, or use its own upload service. Default implementations -/// pass files through unchanged and upload via the WordPress REST API. +/// A delegate only changes *bytes* — GutenbergKit still uploads the result to the +/// configured site and owns the whole lifecycle (retries, cleanup). Because it never +/// performs the upload itself, it cannot deliver media to the wrong place. Set +/// ``EditorViewController/mediaUploadDelegate`` to resize images, transcode video, +/// strip EXIF, etc. +/// +/// This is the safe, common extension point: most hosts want only this. To perform +/// the upload yourself, conform to ``MediaUploader`` instead. public protocol MediaUploadDelegate: AnyObject, Sendable { - /// Whether this delegate might handle a file with the given metadata — either - /// processing it (``processFile(at:mimeType:filename:)``) or uploading it - /// itself (``uploadFile(at:mimeType:filename:)``). + /// Whether this delegate might transform a file with the given metadata. /// /// A cheap, metadata-only gate the server consults *before* materializing the /// upload to a temp file. Return `false` to decline a file by type — e.g. an /// image-only delegate returning `false` for a video — so the server forwards /// the original upload to WordPress without first copying a file the delegate - /// won't touch. Because it gates the temp-file copy needed by *both* - /// `processFile` and `uploadFile`, return `true` for any file the delegate - /// will either process or upload itself. + /// won't touch. + /// + /// With a ``MediaUploader`` set this can't decline the upload itself — an + /// uploader delivers every file, so there is no passthrough to fall to — but it + /// still gates `processFile`: a declined file reaches the uploader unprocessed. /// /// Defaults to `true`: every file is materialized and the full pipeline runs. /// A `true` here is not a commitment — `processFile` may still return @@ -74,23 +80,6 @@ public protocol MediaUploadDelegate: AnyObject, Sendable { /// file and its metadata. When the format changes, report the new mimeType /// and filename so WordPress stores it with the correct extension and type. func processFile(at url: URL, mimeType: String, filename: String) async throws -> ProcessedProxyFile - - /// Upload a processed file to the remote WordPress site. - /// - /// Return the raw WordPress response (status code + body), which GutenbergKit - /// relays to the editor unchanged, or `nil` to use the internal media client. A - /// host that uploads to WordPress should return the exact response it - /// received so the editor sees a complete attachment object. - /// - /// - Warning: Returning a raw response splits one upload's HTTP across two - /// owners — you perform the `POST`, but GutenbergKit's editor drives the - /// `post-process` retries and orphan cleanup behind it, through the WebView - /// rather than your stack. It also receives no form fields, so an attachment - /// uploaded this way lands unattached to its post. Conform to ``MediaUploader`` - /// instead: it owns the upload end-to-end and receives a ``MediaUpload`` - /// carrying the fields. - @available(*, deprecated, message: "Conform to MediaUploader instead — it owns the upload's retries and receives the editor's form fields.") - func uploadFile(at url: URL, mimeType: String, filename: String) async throws -> MediaUploadResponse? } /// Default implementations. @@ -102,11 +91,6 @@ extension MediaUploadDelegate { public func processFile(at url: URL, mimeType: String, filename: String) async throws -> ProcessedProxyFile { .original } - - @available(*, deprecated, message: "Conform to MediaUploader instead — it owns the upload's retries and receives the editor's form fields.") - public func uploadFile(at url: URL, mimeType: String, filename: String) async throws -> MediaUploadResponse? { - nil - } } /// One of the editor's non-file form fields, as sent with a media upload. diff --git a/ios/Sources/GutenbergKit/Sources/Media/MediaUploadServer.swift b/ios/Sources/GutenbergKit/Sources/Media/MediaUploadServer.swift index 3012c95db..a76b3d025 100644 --- a/ios/Sources/GutenbergKit/Sources/Media/MediaUploadServer.swift +++ b/ios/Sources/GutenbergKit/Sources/Media/MediaUploadServer.swift @@ -29,10 +29,10 @@ final class MediaUploadServer: Sendable { /// Creates and starts a new upload server. /// /// - Parameters: - /// - uploadDelegate: Optional delegate for customizing file processing and upload. + /// - uploadDelegate: Optional delegate for transforming files before upload. /// - uploader: Optional host uploader that performs the upload on its own stack. /// - internalClient: GutenbergKit's own client for the configured site. Delivers - /// uploads when no host uploader or delegate does, and every media delete. + /// uploads when no host uploader does, and every media delete. /// - maxRequestBodySize: The maximum allowed request body size in bytes. /// Requests exceeding this limit receive a 413 response. Defaults to 4 GB. static func start( @@ -284,10 +284,10 @@ final class MediaUploadServer: Sendable { /// Result of the delegate processing + upload pipeline. private enum UploadResult { - /// The uploader, delegate, or internal media client completed the upload; + /// The uploader or internal media client completed the upload; /// carries the raw WordPress response to relay. case uploaded(MediaUploadResponse) - /// The delegate didn't modify the file and `uploadFile` returned nil. + /// The delegate didn't modify the file, so the original body is forwarded. /// The caller should forward the original request body to WordPress. case passthrough } @@ -356,12 +356,7 @@ final class MediaUploadServer: Sendable { return .uploaded(MediaUploadResponse(statusCode: 201, body: attachment)) } - // The deprecated delegate path: the host performs the POST but returns the raw - // response, leaving the editor to drive post-process recovery behind it. - if let delegate = context.uploadDelegate, - let result = try await deprecatedUploadFile(delegate, uploadURL, uploadMimeType, uploadFilename) { - return .uploaded(result) - } else if let internalClient = context.internalClient { + if let internalClient = context.internalClient { // Unmodified — forward the original request body directly, skipping // multipart re-encoding. if case .original = processed { @@ -386,17 +381,6 @@ final class MediaUploadServer: Sendable { return fields } - /// Calls the deprecated `uploadFile` hook from one place. - /// - /// This deliberately leaves one deprecation warning in GutenbergKit's own build: - /// the marker exists to tell *hosts* to migrate, and supporting the hook until it - /// is removed means calling it. The warning marks the code that goes with it. - private static func deprecatedUploadFile( - _ delegate: any MediaUploadDelegate, _ url: URL, _ mimeType: String, _ filename: String - ) async throws -> MediaUploadResponse? { - try await delegate.uploadFile(at: url, mimeType: mimeType, filename: filename) - } - private static func errorResponse(status: Int, message: String) -> HTTPResponse { // Emit a WordPress-REST-style error object so the JS middleware normalizes // it (and surfaces `message`) the same way it does a relayed WordPress @@ -510,7 +494,7 @@ enum UploadError: Error, LocalizedError { var errorDescription: String? { switch self { - case .noUploader: "No upload delegate or internal media client configured" + case .noUploader: "No media uploader or internal media client configured" case .streamReadFailed: "Failed to read upload stream" case .streamWriteFailed: "Failed to write upload to disk" } diff --git a/ios/Tests/GutenbergKitTests/Media/MediaUploadServerTests.swift b/ios/Tests/GutenbergKitTests/Media/MediaUploadServerTests.swift index b9b02288a..3452098ab 100644 --- a/ios/Tests/GutenbergKitTests/Media/MediaUploadServerTests.swift +++ b/ios/Tests/GutenbergKitTests/Media/MediaUploadServerTests.swift @@ -157,10 +157,11 @@ struct MediaUploadServerTests { #expect(httpResponse.value(forHTTPHeaderField: "Content-Type") == "text/plain") } - @Test("calls delegate and returns upload result") - func delegateProcessAndUpload() async throws { - let delegate = MockUploadDelegate() - let server = try await MediaUploadServer.start(uploadDelegate: delegate) + @Test("processes with the delegate, then delivers and relays verbatim") + func delegateProcessThenDeliver() async throws { + let delegate = ResizingDelegate() + let internalClient = MockInternalMediaClient() + let server = try await MediaUploadServer.start(uploadDelegate: delegate, internalClient: internalClient) defer { server.stop() } let boundary = UUID().uuidString @@ -178,17 +179,15 @@ struct MediaUploadServerTests { let httpResponse = try #require(response as? HTTPURLResponse) #expect(httpResponse.statusCode == 201) - #expect(delegate.processFileCalled) - #expect(delegate.uploadFileCalled) - #expect(delegate.lastMimeType == "image/jpeg") - #expect(delegate.lastFilename == "photo.jpg") + // The delegate only transforms; GutenbergKit performs the upload. + #expect(internalClient.uploadCalled) // The server relays WordPress's raw response body verbatim. let object = try JSONSerialization.jsonObject(with: data) let json = try #require(object as? [String: Any]) - #expect(json["id"] as? Int == 42) - #expect(json["source_url"] as? String == "https://example.com/photo.jpg") - #expect(json["media_type"] as? String == "image") + #expect(json["id"] as? Int == 99) + #expect(json["source_url"] as? String == "https://example.com/doc.pdf") + #expect(json["media_type"] as? String == "file") } @Test("uses passthrough when delegate does not modify file") @@ -452,9 +451,9 @@ struct MediaUploadServerTests { #expect(received.query == "?_embed=wp:featuredmedia") } - @Test("an uploader takes precedence over the deprecated uploadFile hook") - func uploaderWinsOverDeprecatedHook() async throws { - let delegate = MockUploadDelegate() + @Test("a delegate still processes the file an uploader delivers") + func delegateProcessesForUploader() async throws { + let delegate = ProcessOnlyDelegate() let uploader = RecordingUploader() let server = try await MediaUploadServer.start(uploadDelegate: delegate, uploader: uploader, internalClient: MockInternalMediaClient()) defer { server.stop() } @@ -472,7 +471,6 @@ struct MediaUploadServerTests { // The delegate still processes; only delivery moves to the uploader. #expect(delegate.processFileCalled) - #expect(!delegate.uploadFileCalled) #expect(uploader.received != nil) } @@ -531,9 +529,9 @@ struct MediaUploadServerTests { @Test("retains the delegate for the server's lifetime, and releases it after") func retainsDelegateForServerLifetime() async throws { - weak var weakDelegate: MockUploadDelegate? + weak var weakDelegate: ProcessOnlyDelegate? do { - let delegate = MockUploadDelegate() + let delegate = ProcessOnlyDelegate() weakDelegate = delegate let server = try await MediaUploadServer.start(uploadDelegate: delegate) defer { server.stop() } @@ -553,8 +551,8 @@ struct MediaUploadServerTests { @Test("still processes for a delegate the host has dropped its reference to") func processesForHostReleasedDelegate() async throws { - // The delegate is read at the admission gate and again at processFile and - // uploadFile, separated by a synchronous disk copy and an unbounded processFile. + // The delegate is read at the admission gate and again at processFile, separated + // by a synchronous disk copy and an unbounded processFile. // Held weakly, a host that dropped its reference changed the answer between // those reads: a file admitted for processing was forwarded unprocessed. The // host dropping it before the request is the same condition, deterministically. @@ -1016,36 +1014,6 @@ private final class TranscodingDelegate: MediaUploadDelegate, @unchecked Sendabl } } -private final class MockUploadDelegate: MediaUploadDelegate, @unchecked Sendable { - private let lock = NSLock() - private var _processFileCalled = false - private var _uploadFileCalled = false - private var _lastMimeType: String? - private var _lastFilename: String? - - var processFileCalled: Bool { lock.withLock { _processFileCalled } } - var uploadFileCalled: Bool { lock.withLock { _uploadFileCalled } } - var lastMimeType: String? { lock.withLock { _lastMimeType } } - var lastFilename: String? { lock.withLock { _lastFilename } } - - func processFile(at url: URL, mimeType: String, filename: String) async throws -> ProcessedProxyFile { - lock.withLock { - _processFileCalled = true - _lastMimeType = mimeType - } - return .original - } - - func uploadFile(at url: URL, mimeType: String, filename: String) async throws -> MediaUploadResponse? { - lock.withLock { - _uploadFileCalled = true - _lastFilename = filename - } - let json = #"{"id":42,"source_url":"https://example.com/photo.jpg","media_type":"image"}"# - return MediaUploadResponse(statusCode: 201, body: Data(json.utf8)) - } -} - private final class ProcessOnlyDelegate: MediaUploadDelegate, @unchecked Sendable { private let lock = NSLock() private var _processFileCalled = false