diff --git a/Sources/NextcloudKit/Models/NKLock.swift b/Sources/NextcloudKit/Models/NKLock.swift index 182a14ca..487d3d36 100644 --- a/Sources/NextcloudKit/Models/NKLock.swift +++ b/Sources/NextcloudKit/Models/NKLock.swift @@ -47,6 +47,11 @@ public struct NKLock: Equatable, Sendable { /// public let token: String? + /// + /// Normalized resource ETag returned by the LOCK response. + /// + public let etag: String? + /// /// Initialize from a SwiftyXML accessor. /// @@ -90,6 +95,8 @@ public struct NKLock: Equatable, Sendable { self.time = Date(timeIntervalSince1970: rawTime) self.timeOut = Date(timeIntervalSince1970: rawTime + rawTimeOut) self.token = lockToken + self.etag = properties["d:getetag"].text? + .trimmingCharacters(in: CharacterSet(charactersIn: "\"")) } /// @@ -103,7 +110,7 @@ public struct NKLock: Equatable, Sendable { /// /// Initialize from raw values. /// - public init(owner: String, ownerEditor: String, ownerType: NKLockType, ownerDisplayName: String, time: Date?, timeOut: Date?, token: String?) { + public init(owner: String, ownerEditor: String, ownerType: NKLockType, ownerDisplayName: String, time: Date?, timeOut: Date?, token: String?, etag: String? = nil) { self.owner = owner self.ownerEditor = ownerEditor self.ownerType = ownerType @@ -111,5 +118,6 @@ public struct NKLock: Equatable, Sendable { self.time = time self.timeOut = timeOut self.token = token + self.etag = etag } } diff --git a/Tests/NextcloudKitUnitTests/NKLockUnitTests.swift b/Tests/NextcloudKitUnitTests/NKLockUnitTests.swift new file mode 100644 index 00000000..7e1d4257 --- /dev/null +++ b/Tests/NextcloudKitUnitTests/NKLockUnitTests.swift @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import Testing +@testable import NextcloudKit + +@Suite("NKLock parsing") +struct NKLockUnitTests { + private func makeLockData(etagElement: String = "") -> Data { + let xml = """ + + + 1 + user-id + text + \(NKLockType.token.rawValue) + User Name + 1 + 60 + files_lock/test-token + \(etagElement) + + """ + + return Data(xml.utf8) + } + + @Test("Parses quoted ETag from LOCK response") + func parsesQuotedETag() { + let lock = NKLock(data: makeLockData(etagElement: "\"etag-after-lock\"")) + + #expect(lock != nil) + #expect(lock?.token == "files_lock/test-token") + #expect(lock?.etag == "etag-after-lock") + } + + @Test("Missing ETag keeps lock parsing valid") + func missingETagDoesNotFailParsing() { + let lock = NKLock(data: makeLockData()) + + #expect(lock != nil) + #expect(lock?.etag == nil) + } + + @Test("Unquoted ETag is preserved") + func preservesUnquotedETag() { + let lock = NKLock(data: makeLockData(etagElement: "etag-after-lock")) + + #expect(lock != nil) + #expect(lock?.etag == "etag-after-lock") + } +}