Adds the presigned-upload plumbing Outline's own web client uses for any file upload, not just avatars: attachments.create requests an upload target (server-assigned key, ACL, a short-lived signed form), then a direct multipart POST to that target (uploadUrl/form) actually uploads the bytes — confirmed against a live server's own request/ response shapes pulled from network capture. MultipartFormDataBuilder is a pure, fully-tested function (no network) building that S3-style presigned-POST body. uploadAttachmentFile deliberately sends no Bearer/CSRF header — the presigned form's `sig` field is what authorizes that specific request, same as an S3 presigned POST; flagged as best-effort pending live confirmation, cheap to add if the server turns out to also want one. UpdateUserAvatarRequest (users.update, avatar only) needed a custom encode(to:) — Swift's synthesized Encodable omits nil Optional keys via encodeIfPresent, but removing an avatar needs a literal "avatarUrl": null in the body, not the key missing. Confirmed the request shape (id + avatarUrl) from the captured request's Content-Length matching that shape and no shorter alternative. 6 new tests (multipart body structure/field ordering, explicit-null encoding, create/upload/update round trip) — 63/63 passing.
49 lines
2.0 KiB
Swift
49 lines
2.0 KiB
Swift
import XCTest
|
|
@testable import OutlineKit
|
|
|
|
final class MultipartFormDataBuilderTests: XCTestCase {
|
|
func testBuildIncludesEveryFieldAndTheFile() throws {
|
|
let fileData = Data("fake-jpeg-bytes".utf8)
|
|
let (body, contentType) = MultipartFormDataBuilder.build(
|
|
fields: ["key": "uploads/abc", "acl": "public-read", "Content-Type": "image/jpeg"],
|
|
fileFieldName: "file",
|
|
fileName: "avatar.jpg",
|
|
fileData: fileData,
|
|
fileContentType: "image/jpeg",
|
|
boundary: "TestBoundary"
|
|
)
|
|
|
|
let bodyString = String(decoding: body, as: UTF8.self)
|
|
|
|
XCTAssertEqual(contentType, "multipart/form-data; boundary=TestBoundary")
|
|
XCTAssertTrue(bodyString.contains("Content-Disposition: form-data; name=\"key\""))
|
|
XCTAssertTrue(bodyString.contains("uploads/abc"))
|
|
XCTAssertTrue(bodyString.contains("Content-Disposition: form-data; name=\"acl\""))
|
|
XCTAssertTrue(bodyString.contains("public-read"))
|
|
XCTAssertTrue(bodyString.contains("Content-Disposition: form-data; name=\"file\"; filename=\"avatar.jpg\""))
|
|
XCTAssertTrue(bodyString.contains("fake-jpeg-bytes"))
|
|
XCTAssertTrue(bodyString.hasPrefix("--TestBoundary\r\n"))
|
|
XCTAssertTrue(bodyString.hasSuffix("--TestBoundary--\r\n"))
|
|
}
|
|
|
|
func testFileFieldComesAfterAllFormFields() throws {
|
|
let (body, _) = MultipartFormDataBuilder.build(
|
|
fields: ["a": "1", "b": "2"],
|
|
fileFieldName: "file",
|
|
fileName: "x.jpg",
|
|
fileData: Data("bytes".utf8),
|
|
fileContentType: "image/jpeg",
|
|
boundary: "B"
|
|
)
|
|
let bodyString = String(decoding: body, as: UTF8.self)
|
|
|
|
let fieldsRange = bodyString.range(of: "name=\"a\"")
|
|
let fileRange = bodyString.range(of: "name=\"file\"")
|
|
XCTAssertNotNil(fieldsRange)
|
|
XCTAssertNotNil(fileRange)
|
|
if let fieldsRange, let fileRange {
|
|
XCTAssertTrue(fieldsRange.lowerBound < fileRange.lowerBound)
|
|
}
|
|
}
|
|
}
|