@@ -52,6 +52,80 @@ private slots:
5252 QCOMPARE (fakeFolder.currentLocalState (), fakeFolder.currentRemoteState ());
5353 }
5454
55+ // Verify that a file blocked from uploading by a remote storage quota error is
56+ // protected from local deletion when the remote parent folder is subsequently deleted.
57+ void testQuotaBlockedFileProtectedFromParentFolderDeletion ()
58+ {
59+ FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12 ()};
60+
61+ // Establish an initial clean sync: a1 and a2 exist on both sides.
62+ QVERIFY (fakeFolder.syncOnce ());
63+ QCOMPARE (fakeFolder.currentLocalState (), fakeFolder.currentRemoteState ());
64+
65+ // Add a new local file that will fail to upload due to remote quota (HTTP 507).
66+ fakeFolder.localModifier ().insert (QStringLiteral (" A/quota_blocked.txt" ), 100 );
67+ fakeFolder.serverErrorPaths ().append (QStringLiteral (" A/quota_blocked.txt" ), 507 );
68+
69+ // Sync: upload fails → file is blacklisted as InsufficientRemoteStorage.
70+ QVERIFY (!fakeFolder.syncOnce ());
71+ {
72+ auto entry = fakeFolder.syncJournal ().errorBlacklistEntry (QStringLiteral (" A/quota_blocked.txt" ));
73+ QVERIFY (entry.isValid ());
74+ QCOMPARE (entry._errorCategory , SyncJournalErrorBlacklistRecord::InsufficientRemoteStorage);
75+ }
76+
77+ // The file was never uploaded.
78+ QVERIFY (!fakeFolder.currentRemoteState ().find (QStringLiteral (" A/quota_blocked.txt" )));
79+
80+ // Remove the server error so further requests to that path don't return 507.
81+ fakeFolder.serverErrorPaths ().clear ();
82+
83+ // Server-side: folder A is moved/deleted (e.g., via the web interface).
84+ fakeFolder.remoteModifier ().remove (QStringLiteral (" A" ));
85+
86+ ItemCompletedSpy completeSpy (fakeFolder);
87+ fakeFolder.syncOnce ();
88+
89+ // *** Core assertion: quota-blocked file must NOT have been deleted locally. ***
90+ QVERIFY (fakeFolder.currentLocalState ().find (QStringLiteral (" A/quota_blocked.txt" )));
91+
92+ // Parent folder A must also survive because it still contains the protected file.
93+ QVERIFY (fakeFolder.currentLocalState ().find (QStringLiteral (" A" )));
94+
95+ // Previously-synced siblings must have been deleted locally (trust the server).
96+ QVERIFY (!fakeFolder.currentLocalState ().find (QStringLiteral (" A/a1" )));
97+ QVERIFY (!fakeFolder.currentLocalState ().find (QStringLiteral (" A/a2" )));
98+
99+ // The quota-blocked file must still not exist on the server.
100+ QVERIFY (!fakeFolder.currentRemoteState ().find (QStringLiteral (" A/quota_blocked.txt" )));
101+
102+ // The protected file must be reported as an error (or blacklisted-error) item, not silently ignored.
103+ {
104+ auto item = completeSpy.findItem (QStringLiteral (" A/quota_blocked.txt" ));
105+ QVERIFY (item);
106+ const bool isErrorItem = item->_instruction == CSYNC_INSTRUCTION_ERROR
107+ || item->_instruction == CSYNC_INSTRUCTION_IGNORE; // BlacklistedError reuses IGNORE
108+ QVERIFY (isErrorItem);
109+ const bool isErrorStatus = item->_status == SyncFileItem::SoftError
110+ || item->_status == SyncFileItem::BlacklistedError;
111+ QVERIFY (isErrorStatus);
112+ }
113+ }
114+
115+ // Regression: non-quota new files in a server-deleted folder must still be deleted locally
116+ void testDeleteDirectoryWithNewFileNoQuotaError ()
117+ {
118+ FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12 ()};
119+ fakeFolder.remoteModifier ().remove (QStringLiteral (" A" ));
120+ fakeFolder.localModifier ().insert (QStringLiteral (" A/newfile.txt" ), 100 );
121+
122+ QVERIFY (fakeFolder.syncOnce ());
123+
124+ // New local file with no quota blacklist entry must be removed when parent is deleted on server.
125+ QVERIFY (!fakeFolder.currentLocalState ().find (QStringLiteral (" A/newfile.txt" )));
126+ QCOMPARE (fakeFolder.currentLocalState (), fakeFolder.currentRemoteState ());
127+ }
128+
55129 void issue1329 ()
56130 {
57131 FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12 () };
0 commit comments