Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ clarify uncertainty before coding, and align suggestions with the rules linked b
- Use commits in `<type>(<scope>): summary` format; squash fixups locally before sharing

## Testing
[Fill in by LLM assistant]
- `swift test`: Run the SwiftPM test suite

## Environment
[Fill in by LLM assistant]
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 3.8.0

## Breaking changes
- `TUSClientError.couldNotGetFileStatus` now includes an `underlyingError` for better diagnostics.

# 3.7.0

- Removed cocoapods support
Expand Down
6 changes: 3 additions & 3 deletions Sources/TUSKit/TUSClientError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public enum TUSClientError: Error, LocalizedError {
case couldNotStoreFileMetadata(underlyingError: Error)
case couldNotCreateFileOnServer(underlyingError: Error)
case couldNotUploadFile(underlyingError: Error)
case couldNotGetFileStatus
case couldNotGetFileStatus(underlyingError: Error)
case fileSizeMismatchWithServer
case couldNotDeleteFile(underlyingError: Error)
case uploadIsAlreadyFinished
Expand Down Expand Up @@ -40,8 +40,8 @@ public enum TUSClientError: Error, LocalizedError {
return "Could not create file on server: (\(underlyingError.localizedDescription))"
case .couldNotUploadFile(let underlyingError):
return "Could not upload file: \(underlyingError.localizedDescription)"
case .couldNotGetFileStatus:
return "Could not get file status."
case .couldNotGetFileStatus(let underlyingError):
return "Could not get file status: \(underlyingError.localizedDescription)"
case .fileSizeMismatchWithServer:
return "File size mismatch with server."
case .couldNotDeleteFile(let underlyingError):
Expand Down
2 changes: 1 addition & 1 deletion Sources/TUSKit/Tasks/StatusTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ final class StatusTask: IdentifiableTask {
} catch let error as TUSClientError {
completed(.failure(error))
} catch {
completed(.failure(TUSClientError.couldNotGetFileStatus))
completed(.failure(TUSClientError.couldNotGetFileStatus(underlyingError: error)))
}
}
}
Expand Down
25 changes: 5 additions & 20 deletions Tests/TUSKitTests/FilesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import XCTest
final class FilesTests: XCTestCase {

var files: Files!
var storagePath: URL!

override func setUp() {
super.setUp()

do {
files = try Files(storageDirectory: URL(string: "TUS")!)
storagePath = URL(string: "TUS-\(UUID().uuidString)")!
files = try Files(storageDirectory: storagePath)
} catch {
XCTFail("Could not instantiate Files")
}
Expand All @@ -17,25 +20,11 @@ final class FilesTests: XCTestCase {
override func tearDown() {
do {
try files.clearCacheInStorageDirectory()
try emptyCacheDir()
} catch {
// Okay if dir doesn't exist
}
}

private func emptyCacheDir() throws {

let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
guard FileManager.default.fileExists(atPath: cacheDirectory.path, isDirectory: nil) else {
return
}

for file in try FileManager.default.contentsOfDirectory(atPath: cacheDirectory.path) {
try FileManager.default.removeItem(atPath: cacheDirectory.appendingPathComponent(file).path)
}

}

func testInitializers() {
func removeTrailingSlash(url: URL) -> String {
if url.absoluteString.last == "/" {
Expand Down Expand Up @@ -127,11 +116,7 @@ final class FilesTests: XCTestCase {
// Normally we write to the documents dir. But we explicitly are storing a file in a "wrong dir"
// To see if retrieving metadata updates its directory.
func writeDummyFileToCacheDir() throws -> URL {
let cacheURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent(Bundle.main.bundleIdentifier ?? "")
.appendingPathComponent("TUS")
let fileURL = cacheURL.appendingPathComponent("abcdefgh.txt")
return fileURL
files.storageDirectory.appendingPathComponent("abcdefgh.txt")
}

func storeMetaData(filePath: URL) throws -> URL {
Expand Down
90 changes: 76 additions & 14 deletions Tests/TUSKitTests/Mocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,36 +72,88 @@ final class TUSMockDelegate: TUSClientDelegate {
}
}

typealias Headers = [String: String]?
typealias Headers = [String: String]?

/// MockURLProtocol to support mocking the network
final class MockURLProtocol: URLProtocol {

private static let queue = DispatchQueue(label: "com.tuskit.mockurlprotocol")
static let testIDHeader = "X-Mock-Test-ID"

private struct ResponseKey: Hashable {
let method: String
let testID: String?
}

private struct RequestRecord {
let testID: String?
let request: URLRequest
}

struct Response {
let status: Int
let headers: [String: String]
let data: Data?
}

static var responses = [String: (Headers) -> Response]()
static var receivedRequests = [URLRequest]()
private static var responses = [ResponseKey: (Headers) -> Response]()
private static var requestRecords = [RequestRecord]()

static var receivedRequests: [URLRequest] {
get {
queue.sync {
requestRecords.map(\.request)
}
}
set {
queue.sync {
requestRecords = newValue.map { request in
RequestRecord(testID: request.value(forHTTPHeaderField: testIDHeader), request: request)
}
}
}
}

static func receivedRequests(testID: String?) -> [URLRequest] {
queue.sync {
requestRecords
.filter { $0.testID == testID }
.map(\.request)
}
}

static func reset() {
queue.async {
responses = [:]
receivedRequests = []
static func reset(testID: String? = nil) {
queue.sync {
guard let testID else {
responses = [:]
requestRecords = []
return
}

responses = responses.filter { $0.key.testID != testID }
requestRecords.removeAll { $0.testID == testID }
}
}

static func clearReceivedRequests(testID: String? = nil) {
queue.sync {
guard let testID else {
requestRecords = []
return
}

requestRecords.removeAll { $0.testID == testID }
}
}

/// Define a response to be used for a method
/// - Parameters:
/// - method: The http method (POST PATCH etc)
/// - makeResponse: A closure that returns a Response
static func prepareResponse(for method: String, makeResponse: @escaping (Headers) -> Response) {
queue.async {
responses[method] = makeResponse
static func prepareResponse(for method: String, testID: String? = nil, makeResponse: @escaping (Headers) -> Response) {
let key = ResponseKey(method: method, testID: testID)
queue.sync {
responses[key] = makeResponse
}
}

Expand All @@ -123,17 +175,27 @@ final class MockURLProtocol: URLProtocol {
guard let client = self.client else { return }

guard let method = self.request.httpMethod,
let preparedResponseClosure = type(of: self).responses[method] else {
let preparedResponseClosure = {
let testID = self.request.value(forHTTPHeaderField: type(of: self).testIDHeader)
let key = ResponseKey(method: method, testID: testID)
let fallbackKey = ResponseKey(method: method, testID: nil)
return type(of: self).responses[key] ?? type(of: self).responses[fallbackKey]
}() else {
// assertionFailure("No response found for \(String(describing: request.httpMethod)) prepared \(type(of: self).responses)")
return
}

let preparedResponse = preparedResponseClosure(self.request.allHTTPHeaderFields)

type(of: self).receivedRequests.append(self.request)
type(of: self).requestRecords.append(
RequestRecord(
testID: self.request.value(forHTTPHeaderField: type(of: self).testIDHeader),
request: self.request
)
)

let url = URL(string: "https://tusd.tusdemo.net/files")!
let response = HTTPURLResponse(url: url, statusCode: preparedResponse.status, httpVersion: nil, headerFields: preparedResponse.headers)!
let responseURL = self.request.url ?? URL(string: "https://tusd.tusdemo.net/files")!
let response = HTTPURLResponse(url: responseURL, statusCode: preparedResponse.status, httpVersion: nil, headerFields: preparedResponse.headers)!

client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)

Expand Down
32 changes: 16 additions & 16 deletions Tests/TUSKitTests/Support/NetworkSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import XCTest

/// Server gives inappropriorate offsets
/// - Parameter data: Data to upload
func prepareNetworkForWrongOffset(data: Data) {
MockURLProtocol.prepareResponse(for: "POST") { _ in
func prepareNetworkForWrongOffset(data: Data, testID: String? = nil) {
MockURLProtocol.prepareResponse(for: "POST", testID: testID) { _ in
MockURLProtocol.Response(status: 200, headers: ["Location": "www.somefakelocation.com"], data: nil)
}

// Mimick chunk uploading with offsets
MockURLProtocol.prepareResponse(for: "PATCH") { headers in
MockURLProtocol.prepareResponse(for: "PATCH", testID: testID) { headers in

guard let headers = headers,
let strOffset = headers["Upload-Offset"],
Expand All @@ -27,8 +27,8 @@ func prepareNetworkForWrongOffset(data: Data) {
}
}

func prepareNetworkForSuccesfulUploads(data: Data, lowerCasedKeysInResponses: Bool = false) {
MockURLProtocol.prepareResponse(for: "POST") { _ in
func prepareNetworkForSuccesfulUploads(data: Data, lowerCasedKeysInResponses: Bool = false, testID: String? = nil) {
MockURLProtocol.prepareResponse(for: "POST", testID: testID) { _ in
let key: String
if lowerCasedKeysInResponses {
key = "location"
Expand All @@ -39,7 +39,7 @@ func prepareNetworkForSuccesfulUploads(data: Data, lowerCasedKeysInResponses: Bo
}

// Mimick chunk uploading with offsets
MockURLProtocol.prepareResponse(for: "PATCH") { headers in
MockURLProtocol.prepareResponse(for: "PATCH", testID: testID) { headers in

guard let headers = headers,
let strOffset = headers["Upload-Offset"],
Expand All @@ -64,33 +64,33 @@ func prepareNetworkForSuccesfulUploads(data: Data, lowerCasedKeysInResponses: Bo

}

func prepareNetworkForErronousResponses() {
MockURLProtocol.prepareResponse(for: "POST") { _ in
func prepareNetworkForErronousResponses(testID: String? = nil) {
MockURLProtocol.prepareResponse(for: "POST", testID: testID) { _ in
MockURLProtocol.Response(status: 401, headers: [:], data: nil)
}
MockURLProtocol.prepareResponse(for: "PATCH") { _ in
MockURLProtocol.prepareResponse(for: "PATCH", testID: testID) { _ in
MockURLProtocol.Response(status: 401, headers: [:], data: nil)
}
MockURLProtocol.prepareResponse(for: "HEAD") { _ in
MockURLProtocol.prepareResponse(for: "HEAD", testID: testID) { _ in
MockURLProtocol.Response(status: 401, headers: [:], data: nil)
}
}

func prepareNetworkForSuccesfulStatusCall(data: Data) {
MockURLProtocol.prepareResponse(for: "HEAD") { _ in
func prepareNetworkForSuccesfulStatusCall(data: Data, testID: String? = nil) {
MockURLProtocol.prepareResponse(for: "HEAD", testID: testID) { _ in
MockURLProtocol.Response(status: 200, headers: ["Upload-Length": String(data.count),
"Upload-Offset": "0"], data: nil)
}
}

/// Create call can still succeed. This is useful for triggering a status call.
func prepareNetworkForFailingUploads() {
func prepareNetworkForFailingUploads(testID: String? = nil) {
// Upload means patch. Letting that fail.
MockURLProtocol.prepareResponse(for: "PATCH") { _ in
MockURLProtocol.prepareResponse(for: "PATCH", testID: testID) { _ in
MockURLProtocol.Response(status: 401, headers: [:], data: nil)
}
}

func resetReceivedRequests() {
MockURLProtocol.receivedRequests = []
func resetReceivedRequests(testID: String? = nil) {
MockURLProtocol.clearReceivedRequests(testID: testID)
}
12 changes: 10 additions & 2 deletions Tests/TUSKitTests/Support/Support.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,23 @@ func clearDirectory(dir: URL) {
}
}

func makeClient(storagePath: URL?, supportedExtensions: [TUSProtocolExtension] = [.creation]) -> TUSClient {
func makeClient(storagePath: URL?,
supportedExtensions: [TUSProtocolExtension] = [.creation],
sessionIdentifier: String = "TEST",
mockTestID: String? = nil) -> TUSClient {
let liveDemoPath = URL(string: "https://tusd.tusdemo.net/files")!

// We don't use a live URLSession, we mock it out.
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [MockURLProtocol.self]
if let mockTestID {
var headers = configuration.httpAdditionalHeaders as? [String: String] ?? [:]
headers[MockURLProtocol.testIDHeader] = mockTestID
configuration.httpAdditionalHeaders = headers
}
do {
let client = try TUSClient(server: liveDemoPath,
sessionIdentifier: "TEST",
sessionIdentifier: sessionIdentifier,
sessionConfiguration: configuration,
storageDirectory: storagePath,
supportedExtensions: supportedExtensions)
Expand Down
Loading