Skip to content
Open
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
20 changes: 18 additions & 2 deletions Nextcloud.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
2C1D5D7923E2DE9100334ABB /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; };
2C33C48223E2C475005F963B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C33C48123E2C475005F963B /* NotificationService.swift */; };
2C33C48623E2C475005F963B /* Notification Service Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2C33C47F23E2C475005F963B /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
2EF7D3FD0885CE182717578E /* NKError+UserFriendlyMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */; };
370D26AF248A3D7A00121797 /* NCCellMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370D26AE248A3D7A00121797 /* NCCellMain.swift */; };
63563FE53573412998852DD3 /* NCAlbumsPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439616AAA50F4360A761E5EF /* NCAlbumsPersistenceTests.swift */; };
822D68B06B101563A759AD59 /* NKError+UserFriendlyMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */; };
A12720261001000000000001 /* NCBackgroundUploadExtensionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7C230417C7100F011FE /* NCBackgroundUploadExtensionManager.swift */; };
A1E2EE010000000000000002 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; };
A1E2EE010000000000000003 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; };
Expand All @@ -28,6 +30,7 @@
A1E2EE020000000000000005 /* NCEndToEndKeySetResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE020000000000000002 /* NCEndToEndKeySetResolver.swift */; };
A1E2EE020000000000000006 /* NCEndToEndKeySetResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE020000000000000002 /* NCEndToEndKeySetResolver.swift */; };
A1E2EE020000000000000008 /* NCEndToEndKeySetResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE020000000000000007 /* NCEndToEndKeySetResolverTests.swift */; };
A358B569BF5FDABA6D9012D1 /* NCErrorContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */; };
A5A87F9E4B0E4441A6A4BC20 /* NCContextMenuProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7697C94BA14450A0867940 /* NCContextMenuProfile.swift */; };
AA3C85E82D36B08C00F74F12 /* UITestBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3C85E72D36B08C00F74F12 /* UITestBackend.swift */; };
AA3C85EB2D36BBFB00F74F12 /* OCSResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3C85EA2D36BBF400F74F12 /* OCSResponse.swift */; };
Expand Down Expand Up @@ -112,13 +115,15 @@
AFCE353727E4ED7B00FEA6C2 /* NCShareCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */; };
AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* Shareable.swift */; };
B601AA012F10000100C0D3E5 /* NCLogFilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B601AA002F10000100C0D3E5 /* NCLogFilesView.swift */; };
BAF64949B487A080A98F83F2 /* NKErrorUserFriendlyMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320C20803A58DAF7575440A8 /* NKErrorUserFriendlyMessageTests.swift */; };
C0DEA070306D000100000005 /* NCLocalDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEA070306D000100000004 /* NCLocalDatabase+AutoUpload.swift */; };
C0DEA070306D000100000006 /* NCLocalDatabase+AutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEA070306D000100000004 /* NCLocalDatabase+AutoUpload.swift */; };
C0DEA070306D000100000008 /* NCAutoUploadAssetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEA070306D000100000007 /* NCAutoUploadAssetTests.swift */; };
C0DECA022F65000100C0D001 /* NCCameraRollTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */; };
C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */; };
CB3666201AF7550816B5CD6A /* NCContextMenuComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */; };
D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; };
EB511D22564B7B9A605E70AE /* NCErrorContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */; };
F0A1B2C430B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C330B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift */; };
F0A1B2C630B6000100D4E5F6 /* NCImageZoomViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C530B6000100D4E5F6 /* NCImageZoomViewTests.swift */; };
F31165022F9674A1009A1E37 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = F31165012F9674A1009A1E37 /* AppIcon.icon */; };
Expand Down Expand Up @@ -1334,9 +1339,11 @@
2C33C47F23E2C475005F963B /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
2C33C48123E2C475005F963B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
2C33C48A23E2CC26005F963B /* Notification_Service_Extension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Notification_Service_Extension-Bridging-Header.h"; sourceTree = "<group>"; };
320C20803A58DAF7575440A8 /* NKErrorUserFriendlyMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NKErrorUserFriendlyMessageTests.swift; sourceTree = "<group>"; };
370D26AE248A3D7A00121797 /* NCCellMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCellMain.swift; sourceTree = "<group>"; };
439616AAA50F4360A761E5EF /* NCAlbumsPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAlbumsPersistenceTests.swift; sourceTree = "<group>"; };
4B74B947E07444CC9CFE0A6C /* NCDocumentEditorSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCDocumentEditorSupportTests.swift; sourceTree = "<group>"; };
79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKError+UserFriendlyMessage.swift"; sourceTree = "<group>"; };
8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuComment.swift; sourceTree = "<group>"; };
A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndKeySet.swift; sourceTree = "<group>"; };
A1E2EE010000000000000009 /* NCEndToEndKeySetArchiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndKeySetArchiveTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1480,6 +1487,7 @@
C0DEA070306D000100000007 /* NCAutoUploadAssetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUploadAssetTests.swift; sourceTree = "<group>"; };
C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCameraRollTests.swift; sourceTree = "<group>"; };
C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUploadDateTests.swift; sourceTree = "<group>"; };
CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCErrorContext.swift; sourceTree = "<group>"; };
D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCActivityTableViewCell.swift; sourceTree = "<group>"; };
F0A1B2C330B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaViewerFloatingTitleViewTests.swift; sourceTree = "<group>"; };
F0A1B2C530B6000100D4E5F6 /* NCImageZoomViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCImageZoomViewTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2388,6 +2396,7 @@
children = (
AC13ADAF2026101300000001 /* NCMediaAdaptiveLayoutViewTests.swift */,
C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */,
320C20803A58DAF7575440A8 /* NKErrorUserFriendlyMessageTests.swift */,
C0DEA070306D000100000007 /* NCAutoUploadAssetTests.swift */,
F76BED7B306D0000002E1442 /* NCPhotoLibraryAssetTests.swift */,
A1E2EE010000000000000009 /* NCEndToEndKeySetArchiveTests.swift */,
Expand Down Expand Up @@ -2814,6 +2823,8 @@
F785129A2D79899E0087DDD0 /* NCNetworking+TermsOfService.swift */,
F71916102E2901E800E13E96 /* NCNetworking+Upload.swift */,
F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */,
79DFE0BDF3898A66910E67CA /* NKError+UserFriendlyMessage.swift */,
CE0CD9A00D74B01055F238E9 /* NCErrorContext.swift */,
F70D8D8024A4A9BF000A5756 /* NCNetworkingProcess.swift */,
F755BD9A20594AC7008C5FBB /* NCService.swift */,
);
Expand Down Expand Up @@ -4851,6 +4862,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BAF64949B487A080A98F83F2 /* NKErrorUserFriendlyMessageTests.swift in Sources */,
AC13ADAF2026101300000002 /* NCMediaAdaptiveLayoutViewTests.swift in Sources */,
C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */,
C0DEA070306D000100000008 /* NCAutoUploadAssetTests.swift in Sources */,
Expand Down Expand Up @@ -4944,6 +4956,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2EF7D3FD0885CE182717578E /* NKError+UserFriendlyMessage.swift in Sources */,
EB511D22564B7B9A605E70AE /* NCErrorContext.swift in Sources */,
F7864ACF2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift in Sources */,
F71F6D0A2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */,
F7245925289BB59100474787 /* ThreadSafeDictionary.swift in Sources */,
Expand Down Expand Up @@ -5206,6 +5220,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
822D68B06B101563A759AD59 /* NKError+UserFriendlyMessage.swift in Sources */,
A358B569BF5FDABA6D9012D1 /* NCErrorContext.swift in Sources */,
F76BED77306D0000002E1442 /* NCLocalDatabase.swift in Sources */,
F76BED78306D0000002E1442 /* NCLocalDatabaseMigrator.swift in Sources */,
F76BED7C306D0000002E1442 /* NCLocalDatabase+PhotoLibraryAsset.swift in Sources */,
Expand Down Expand Up @@ -7279,8 +7295,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nextcloud/NextcloudKit";
requirement = {
kind = exactVersion;
version = 7.6.2;
branch = "friendly-errors";
kind = branch;
};
};
F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 118 additions & 0 deletions Tests/NextcloudUnitTests/NKErrorUserFriendlyMessageTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2026 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation
import NextcloudKit
import Testing
@testable import Nextcloud

@Suite("Error messages shown to people")
struct NKErrorUserFriendlyMessageTests {
static let shownKinds = NKError.Kind.allCases.filter { $0 != .cancelled }

private func localized(_ key: String) -> String {
NSLocalizedString(key, comment: "")
}

@Test("Nothing is shown for success or cancellation")
func silentErrors() {
#expect(NKError.success.userFriendlyMessage() == nil)
#expect(NKError.cancelled.userFriendlyMessage(for: .upload(fileName: "a.jpg")) == nil)
#expect(NKError(errorCode: NCGlobal.shared.errorTaskCancelled, errorDescription: "Task cancelled").userFriendlyMessage() == nil)
}

@Test("Keeps text the app wrote itself")
func appMessages() {
#expect(NKError(errorCode: NCGlobal.shared.errorOfflineNotAllowed, errorDescription: "_offline_not_allowed_").userFriendlyMessage() == localized("_offline_not_allowed_"))
#expect(NKError(errorCode: NCGlobal.shared.errorE2EELock, errorDescription: "Already translated").userFriendlyMessage() == "Already translated")
}

@Test("Replaces technical server text")
func technicalServerText() throws {
let error = NKError(errorCode: 409, errorDescription: "Parent node does not exist")
let generic = try #require(error.userFriendlyMessage())

#expect(error.userFriendlyMessage(for: .createFolder(folderName: "a/b")) == localized("_error_create_folder_no_parent_"))
#expect(generic != error.errorDescription)
#expect(!generic.contains("409"))
}

@Test("Uses the generic message unless the operation needs its own")
func operationMessages() {
let forbidden = NKError(errorCode: 403, errorDescription: "403: Forbidden")
let generic = forbidden.userFriendlyMessage()

#expect(generic != nil)
#expect(forbidden.userFriendlyMessage(for: .delete(fileName: "a.jpg")) == generic)
#expect(forbidden.userFriendlyMessage(for: .rename) == generic)
#expect(forbidden.userFriendlyMessage(for: .upload(fileName: "a.jpg")) == generic)
}

@Test("Recognizes uploads rejected by the virus scanner")
func virusDetected() {
let error = NKError(errorCode: 415, errorDescription: "Virus Eicar-Signature is detected in the file. Upload cannot be completed.")

#expect(error.userFriendlyMessage(for: .upload(fileName: "eicar.com")) == localized("_virus_detect_"))
}

@Test("Words different kinds of failure differently")
func kindMessages() {
let timeout = NKError(errorCode: NSURLErrorTimedOut, errorDescription: "The request timed out.").userFriendlyMessage()
let quota = NKError(errorCode: 507, errorDescription: "507: Storage quota is reached").userFriendlyMessage()
let unknown = NKError(errorCode: 418, errorDescription: "418: I'm a teapot").userFriendlyMessage()

#expect(Set([timeout, quota, unknown]).count == 3)
#expect(quota != "507: Storage quota is reached")
}

@Test("Every kind has a translated message", arguments: NKErrorUserFriendlyMessageTests.shownKinds)
func everyKind(kind: NKError.Kind) {
let message = NKError(errorCode: -1, errorDescription: "Technical", kind: kind).userFriendlyMessage()

#expect(message != nil)
#expect(message != "Technical")
}

@Test("Maps transfer events and saved statuses to operations")
func contexts() {
let global = NCGlobal.shared

#expect(NCErrorContext(networkingStatus: global.networkingStatusUploaded, fileName: "a.jpg") == .upload(fileName: "a.jpg"))
#expect(NCErrorContext(networkingStatus: global.networkingStatusCopyMove, fileName: "a.jpg") == .moveOrCopy(fileName: "a.jpg"))
#expect(NCErrorContext(networkingStatus: global.networkingStatusReloadAvatar, fileName: "") == .generic)
#expect(NCErrorContext(metadataStatus: global.metadataStatusWaitCreateFolder, fileName: "Photos") == .createFolder(folderName: "Photos"))
#expect(NCErrorContext(metadataStatus: global.metadataStatusDownloadError, fileName: "a.jpg") == .download(fileName: "a.jpg"))
}

@Test("Titles say what failed")
func failureTitles() throws {
let contexts: [NCErrorContext] = [.upload(fileName: "f"), .download(fileName: "f"), .delete(fileName: "f"), .rename, .moveOrCopy(fileName: "f"), .createFolder(folderName: "f"), .favorite(fileName: "f")]

#expect(NCErrorContext.generic.failureTitle == nil)
#expect(NCErrorContext.upload(fileName: "IMG_0042.jpg").failureTitle?.contains("IMG_0042.jpg") == true)

for context in contexts {
let title = try #require(context.failureTitle)

#expect(!title.hasPrefix("_"))
}
}

@Test("Words saved transfer errors")
func savedErrors() {
let metadata = tableMetadata()
metadata.fileNameView = "a.jpg"
metadata.status = NCGlobal.shared.metadataStatusUploadError
metadata.errorCode = 507
metadata.sessionError = "507: Storage quota is reached"

#expect(metadata.sessionErrorMessage == NKError(errorCode: 507, errorDescription: "").userFriendlyMessage())

metadata.errorCode = NSURLErrorCancelled
metadata.sessionError = "Transfer was cancelled."

#expect(metadata.sessionErrorMessage != "Transfer was cancelled.")
#expect(!metadata.sessionErrorMessage.isEmpty)
}
}
6 changes: 2 additions & 4 deletions iOSClient/Activity/NCActivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@ class NCActivity: UIViewController, NCSharePagingContent {
} else {
Task {
await showErrorBanner(windowScene: self.windowScene,
text: error.errorDescription,
errorCode: error.errorCode)
error: error)
}
}
}
Expand Down Expand Up @@ -447,8 +446,7 @@ extension NCActivity {
} else if error.errorCode != NCGlobal.shared.errorResourceNotFound {
Task {
await showErrorBanner(windowScene: self.windowScene,
text: error.errorDescription,
errorCode: error.errorCode)
error: error)
}
}

Expand Down
12 changes: 12 additions & 0 deletions iOSClient/Data/NCManageDatabase+Metadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,18 @@ extension tableMetadata {
}
}

#if !EXTENSION || EXTENSION_SHARE
extension tableMetadata {
/// The saved `sessionError`, worded for people from its `errorCode` and the operation the item failed at.
var sessionErrorMessage: String {
let error = NKError(errorCode: errorCode, errorDescription: sessionError)
Comment thread
mpivchev marked this conversation as resolved.
let context = NCErrorContext(metadataStatus: status, fileName: fileNameView)

return error.userFriendlyMessage(for: context) ?? error.localizedGenericMessage
}
}
#endif

extension NCManageDatabase {
#if !EXTENSION
func isMetadataShareOrMounted(metadata: tableMetadata, metadataFolder: tableMetadata?) -> Bool {
Expand Down
Loading
Loading