1 /*
2     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
3     SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
4 */
5 
6 #include <utility>
7 
8 #include <QProcess>
9 #include <QTest>
10 
11 #include <KIO/Job>
12 
13 #include "transfer_resume.h"
14 
15 struct FakeWorker {
16     struct FakeError {
17         const int id;
18         const QString message;
19     };
20 
errorFakeWorker21     void error(int id, const QString &message)
22     {
23         m_errors.push_back(FakeError{id, message});
24     }
25 
configValueFakeWorker26     bool configValue(const QString &key, bool defaultValue) const
27     {
28         return m_config.value(key, defaultValue);
29     }
30 
canResumeFakeWorker31     bool canResume(KIO::filesize_t offset)
32     {
33         Q_UNUSED(offset); // in reality this is a user query, always assume the user says yes for now
34         return true;
35     }
36 
debugErrorsFakeWorker37     void debugErrors()
38     {
39         for (const auto &error : std::as_const(m_errors)) {
40             qDebug() << "ERROR{" << KIO::buildErrorString(error.id, error.message) << "}";
41         }
42     }
43 
44     QVector<FakeError> m_errors;
45     QHash<QString, bool> m_config = {{"MarkPartial", true}};
46 };
47 
48 class ShouldResumeTest : public QObject
49 {
50     Q_OBJECT
51 private:
52     QTemporaryDir m_tmpDir;
53 private Q_SLOTS:
initTestCase()54     void initTestCase()
55     {
56         QVERIFY(m_tmpDir.isValid());
57         const QString fixturesPath = QFINDTESTDATA("fixtures/.");
58         QProcess proc;
59         proc.setProcessChannelMode(QProcess::ForwardedChannels);
60         proc.start("cp", {"-rv", fixturesPath, m_tmpDir.path()});
61         QVERIFY(proc.waitForFinished());
62         QCOMPARE(proc.exitCode(), 0);
63     }
64 
tmpPath(const QString & subpath)65     QString tmpPath(const QString &subpath)
66     {
67         return QDir(m_tmpDir.path()).filePath(subpath);
68     }
69 
tmpUrl(const QString & subpath)70     QUrl tmpUrl(const QString &subpath)
71     {
72         return QUrl::fromLocalFile(tmpPath(subpath));
73     }
74 
noResumeButPartial()75     void noResumeButPartial()
76     {
77         // NB: this has no fixture ;)
78         FakeWorker worker;
79 
80         auto url = tmpUrl("noResumeButPartial/thing");
81         auto partUrl = tmpUrl("noResumeButPartial/thing.part");
82         auto resume = Transfer::shouldResume<QFileResumeIO>(url, KIO::JobFlags(), &worker);
83         QVERIFY(resume.has_value());
84         QCOMPARE(resume->resuming, false);
85         QCOMPARE(resume->destination, partUrl);
86         QCOMPARE(resume->completeDestination, url);
87         QCOMPARE(resume->partDestination, partUrl);
88 
89         QDir().mkdir(tmpPath("noResumeButPartial"));
90         QFile part(partUrl.toLocalFile());
91         QVERIFY(part.open(QFile::WriteOnly));
92         part.write("");
93         QCOMPARE(Transfer::concludeResumeHasError<QFileResumeIO>(false, resume.value(), &worker), false);
94         QVERIFY(QFileInfo::exists(url.toLocalFile()));
95         QVERIFY(!QFileInfo::exists(partUrl.toLocalFile()));
96     }
97 
noResumeAndNoPartial()98     void noResumeAndNoPartial()
99     {
100         // NB: this has no fixture ;)
101         FakeWorker worker;
102         worker.m_config["MarkPartial"] = false;
103 
104 
105         auto url = tmpUrl("noResumeAndNoPartial/thing");
106         auto resume = Transfer::shouldResume<QFileResumeIO>(url, KIO::JobFlags(), &worker);
107         worker.debugErrors();
108         QVERIFY(resume.has_value());
109         QCOMPARE(resume->resuming, false);
110         QCOMPARE(resume->destination, url);
111         QCOMPARE(resume->completeDestination, url);
112         QCOMPARE(resume->partDestination, QUrl());
113         QCOMPARE(Transfer::concludeResumeHasError<QFileResumeIO>(false, resume.value(), &worker), false);
114     }
115 
resume()116     void resume()
117     {
118         FakeWorker worker;
119 
120         auto url = tmpUrl("resume/thing");
121         auto partUrl = tmpUrl("resume/thing.part");
122         auto resume = Transfer::shouldResume<QFileResumeIO>(url, KIO::JobFlags(), &worker);
123         QVERIFY(resume.has_value());
124         QCOMPARE(resume->resuming, true);
125         QCOMPARE(resume->destination, partUrl);
126         QCOMPARE(resume->completeDestination, url);
127         QCOMPARE(resume->partDestination, partUrl);
128 
129         QCOMPARE(Transfer::concludeResumeHasError<QFileResumeIO>(false, resume.value(), &worker), false);
130         QVERIFY(QFileInfo::exists(url.toLocalFile()));
131         QVERIFY(!QFileInfo::exists(partUrl.toLocalFile()));
132     }
133 
resumeInPlace()134     void resumeInPlace()
135     {
136         FakeWorker worker;
137 
138         auto url = tmpUrl("resumeInPlace/thing");
139         auto resume = Transfer::shouldResume<QFileResumeIO>(url, KIO::Resume, &worker);
140         QVERIFY(resume.has_value());
141         QCOMPARE(resume->resuming, true);
142         QCOMPARE(resume->destination, url);
143         QCOMPARE(resume->completeDestination, url);
144         QCOMPARE(resume->partDestination, url);
145 
146         QCOMPARE(Transfer::concludeResumeHasError<QFileResumeIO>(false, resume.value(), &worker), false);
147         QVERIFY(QFileInfo::exists(url.toLocalFile()));
148     }
149 
noResumeInPlace()150     void noResumeInPlace()
151     {
152         FakeWorker worker;
153 
154         auto url = tmpUrl("resumeInPlace/thing"); // intentionally the same path this scenario errors out
155         auto resume = Transfer::shouldResume<QFileResumeIO>(url, KIO::JobFlags(), &worker);
156         QVERIFY(!resume.has_value());
157         QCOMPARE(worker.m_errors.size(), 1);
158         QCOMPARE(worker.m_errors.at(0).id, KIO::ERR_FILE_ALREADY_EXIST);
159     }
160 };
161 
162 QTEST_GUILESS_MAIN(ShouldResumeTest)
163 
164 #include "shouldresumetest.moc"
165