Merge remote-tracking branch 'origin/main' into v2.17.x
[dcpomatic.git] / test / config_test.cc
1 /*
2     Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "lib/cinema.h"
23 #include "lib/cinema_list.h"
24 #include "lib/config.h"
25 #include "lib/dkdm_recipient.h"
26 #include "lib/dkdm_recipient_list.h"
27 #include "lib/unzipper.h"
28 #include "lib/zipper.h"
29 #include "test.h"
30 #include <boost/test/unit_test.hpp>
31 #include <fstream>
32
33
34 using std::make_shared;
35 using std::ofstream;
36 using std::string;
37 using std::vector;
38 using boost::optional;
39
40
41 static string
42 rewrite_bad_config (string filename, string extra_line)
43 {
44         using namespace boost::filesystem;
45
46         auto base = path("build/test/bad_config/2.18");
47         auto file = base / filename;
48
49         boost::system::error_code ec;
50         remove (file, ec);
51
52         boost::filesystem::create_directories (base);
53         std::ofstream f (file.string().c_str());
54         f << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
55           << "<Config>\n"
56           << "<Foo></Foo>\n"
57           << extra_line << "\n"
58           << "</Config>\n";
59         f.close ();
60
61         return dcp::file_to_string (file);
62 }
63
64
65 BOOST_AUTO_TEST_CASE (config_backup_test)
66 {
67         ConfigRestorer cr;
68
69         Config::override_path = "build/test/bad_config";
70         Config::drop();
71         boost::filesystem::remove_all ("build/test/bad_config");
72
73         /* Write an invalid config file to config.xml */
74         auto const first_write_xml = rewrite_bad_config("config.xml", "first write");
75
76         /* Load the config; this should fail, causing the bad config to be copied to config.xml.1
77          * and a new config.xml created in its place.
78          */
79         Config::instance();
80
81         boost::filesystem::path const prefix = "build/test/bad_config/2.18";
82
83         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1"));
84         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml);
85         BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.2"));
86         BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.3"));
87         BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.4"));
88
89         Config::drop();
90         auto const second_write_xml = rewrite_bad_config("config.xml", "second write");
91         Config::instance();
92
93         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1"));
94         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml);
95         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.2"));
96         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.2") == second_write_xml);
97         BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.3"));
98         BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.4"));
99
100         Config::drop();
101         auto const third_write_xml = rewrite_bad_config("config.xml", "third write");
102         Config::instance();
103
104         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1"));
105         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml);
106         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.2"));
107         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.2") == second_write_xml);
108         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.3"));
109         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.3") == third_write_xml);
110         BOOST_CHECK(!boost::filesystem::exists(prefix / "config.xml.4"));
111
112         Config::drop();
113         auto const fourth_write_xml = rewrite_bad_config("config.xml", "fourth write");
114         Config::instance();
115
116         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.1"));
117         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.1") == first_write_xml);
118         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.2"));
119         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.2") == second_write_xml);
120         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.3"));
121         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.3") == third_write_xml);
122         BOOST_CHECK(boost::filesystem::exists(prefix / "config.xml.4"));
123         BOOST_CHECK(dcp::file_to_string(prefix / "config.xml.4") == fourth_write_xml);
124 }
125
126
127 BOOST_AUTO_TEST_CASE (config_backup_with_link_test)
128 {
129         using namespace boost::filesystem;
130
131         ConfigRestorer cr;
132
133         auto base = path("build/test/bad_config");
134         auto version = base / "2.18";
135
136         Config::override_path = base;
137         Config::drop();
138
139         boost::filesystem::remove_all (base);
140
141         boost::filesystem::create_directories (version);
142         std::ofstream f (path(version / "config.xml").string().c_str());
143         f << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
144           << "<Config>\n"
145           << "<Link>" << path(version / "actual.xml").string() << "</Link>\n"
146           << "</Config>\n";
147         f.close ();
148
149         Config::drop ();
150         /* Cause actual.xml to be backed up */
151         rewrite_bad_config ("actual.xml", "first write");
152         Config::instance ();
153
154         /* Make sure actual.xml was backed up to the right place */
155         BOOST_CHECK (boost::filesystem::exists(version / "actual.xml.1"));
156 }
157
158
159 BOOST_AUTO_TEST_CASE (config_write_utf8_test)
160 {
161         ConfigRestorer cr;
162
163         boost::filesystem::remove_all ("build/test/config.xml");
164         boost::filesystem::copy_file ("test/data/utf8_config.xml", "build/test/config.xml");
165         Config::override_path = "build/test";
166         Config::drop ();
167         Config::instance()->write();
168
169         check_text_file ("test/data/utf8_config.xml", "build/test/config.xml");
170 }
171
172
173 /* 2.14 -> 2.18 */
174 BOOST_AUTO_TEST_CASE (config_upgrade_test1)
175 {
176         ConfigRestorer cr;
177
178         boost::filesystem::path dir = "build/test/config_upgrade_test";
179         Config::override_path = dir;
180         Config::drop ();
181         boost::filesystem::remove_all (dir);
182         boost::filesystem::create_directories (dir);
183
184         boost::filesystem::copy_file ("test/data/2.14.config.xml", dir / "config.xml");
185         boost::filesystem::copy_file ("test/data/2.14.cinemas.xml", dir / "cinemas.xml");
186         try {
187                 /* This will fail to read cinemas.xml since the link is to a non-existent directory */
188                 Config::instance();
189         } catch (...) {}
190
191         Config::instance()->write();
192
193         check_xml (dir / "config.xml", "test/data/2.14.config.xml", {});
194         check_xml (dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {});
195 #ifdef DCPOMATIC_WINDOWS
196         /* This file has the windows path for dkdm_recipients.xml (with backslashes) */
197         check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.sqlite.xml", {});
198 #else
199         check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.sqlite.xml", {});
200 #endif
201         /* cinemas.xml is not copied into 2.18 as its format has not changed */
202         BOOST_REQUIRE (!boost::filesystem::exists(dir / "2.18" / "cinemas.xml"));
203 }
204
205
206 /* 2.16 -> 2.18 */
207 BOOST_AUTO_TEST_CASE (config_upgrade_test2)
208 {
209         ConfigRestorer cr;
210
211         boost::filesystem::path dir = "build/test/config_upgrade_test";
212         Config::override_path = dir;
213         Config::drop ();
214         boost::filesystem::remove_all (dir);
215         boost::filesystem::create_directories (dir);
216
217 #ifdef DCPOMATIC_WINDOWS
218         boost::filesystem::copy_file("test/data/2.16.config.windows.xml", dir / "config.xml");
219 #else
220         boost::filesystem::copy_file("test/data/2.16.config.xml", dir / "config.xml");
221 #endif
222         boost::filesystem::copy_file("test/data/2.14.cinemas.xml", dir / "cinemas.xml");
223         try {
224                 /* This will fail to read cinemas.xml since the link is to a non-existent directory */
225                 Config::instance();
226         } catch (...) {}
227
228         Config::instance()->write();
229
230         check_xml(dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {});
231 #ifdef DCPOMATIC_WINDOWS
232         /* This file has the windows path for dkdm_recipients.xml (with backslashes) */
233         check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.xml", {});
234         check_xml(dir / "config.xml", "test/data/2.16.config.windows.xml", {});
235 #else
236         check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.xml", {});
237         check_xml(dir / "config.xml", "test/data/2.16.config.xml", {});
238 #endif
239         /* cinemas.xml is not copied into 2.18 as its format has not changed */
240         BOOST_REQUIRE (!boost::filesystem::exists(dir / "2.18" / "cinemas.xml"));
241 }
242
243
244 BOOST_AUTO_TEST_CASE (config_keep_cinemas_if_making_new_config)
245 {
246         ConfigRestorer cr;
247
248         boost::filesystem::path dir = "build/test/config_keep_cinemas_if_making_new_config";
249         Config::override_path = dir;
250         Config::drop ();
251         boost::filesystem::remove_all (dir);
252         boost::filesystem::create_directories (dir);
253
254         Config::instance()->write();
255
256         CinemaList cinemas;
257         cinemas.add_cinema({"My Great Cinema", {}, "", dcp::UTCOffset()});
258
259         boost::filesystem::copy_file(dir / "cinemas.sqlite3", dir / "backup_for_test.sqlite3");
260
261         Config::drop ();
262         boost::filesystem::remove (dir / "2.18" / "config.xml");
263         Config::instance();
264
265         check_file(dir / "backup_for_test.sqlite3", dir / "cinemas.sqlite3");
266 }
267
268
269 BOOST_AUTO_TEST_CASE(keep_config_if_cinemas_fail_to_load)
270 {
271         ConfigRestorer cr;
272
273         /* Make a new config */
274         boost::filesystem::path dir = "build/test/keep_config_if_cinemas_fail_to_load";
275         Config::override_path = dir;
276         Config::drop();
277         boost::filesystem::remove_all(dir);
278         boost::filesystem::create_directories(dir);
279         Config::instance()->write();
280
281         CinemaList cinema_list;
282         cinema_list.add_cinema(Cinema("Foo", {}, "Bar", dcp::UTCOffset()));
283
284         auto const cinemas = dir / "cinemas.sqlite3";
285
286         /* Back things up */
287         boost::filesystem::copy_file(dir / "2.18" / "config.xml", dir / "config_backup_for_test.xml");
288         boost::filesystem::copy_file(cinemas, dir / "cinemas_backup_for_test.sqlite3");
289
290         /* Corrupt the cinemas */
291         Config::drop();
292         std::ofstream corrupt(cinemas.string().c_str());
293         corrupt << "foo\n";
294         corrupt.close();
295         Config::instance();
296
297         /* We should have the old config.xml */
298         check_text_file(dir / "2.18" / "config.xml", dir / "config_backup_for_test.xml");
299 }
300
301
302 BOOST_AUTO_TEST_CASE(read_cinemas_xml_and_write_sqlite)
303 {
304         ConfigRestorer cr;
305
306         /* Set up a config with an XML cinemas file */
307         boost::filesystem::path dir = "build/test/read_cinemas_xml_and_write_sqlite";
308         boost::filesystem::remove_all(dir);
309         boost::filesystem::create_directories(dir);
310         boost::filesystem::create_directories(dir / "2.18");
311
312         boost::filesystem::copy_file("test/data/cinemas.xml", dir / "cinemas.xml");
313         boost::filesystem::copy_file("test/data/2.18.config.xml", dir / "2.18" / "config.xml");
314         {
315                 Editor editor(dir / "2.18" / "config.xml");
316                 editor.replace(
317                         "/home/realldoesnt/exist/this/path/is/nonsense.xml",
318                         boost::filesystem::canonical(dir / "cinemas.xml").string()
319                         );
320         }
321
322         Config::override_path = dir;
323         Config::drop();
324
325         /* This should make a sqlite3 file containing the recipients from cinemas.xml */
326         Config::instance();
327
328         {
329                 CinemaList test(dir / "cinemas.sqlite3");
330
331                 /* The detailed creation of sqlite3 from XML is tested in cinema_list_test.cc */
332                 auto cinemas = test.cinemas();
333                 BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
334                 BOOST_CHECK_EQUAL(cinemas[0].second.name, "Great");
335                 BOOST_CHECK_EQUAL(cinemas[1].second.name, "classy joint");
336                 BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
337
338                 /* Add another recipient to the sqlite */
339                 test.add_cinema({"The ol' 1-seater", {}, "Quiet but lonely", dcp::UTCOffset()});
340         }
341
342         /* Reload the config; the old XML should not clobber the new sqlite3 */
343         Config::drop();
344         Config::instance();
345
346         {
347                 CinemaList test(dir / "cinemas.sqlite3");
348
349                 auto cinemas = test.cinemas();
350                 BOOST_REQUIRE_EQUAL(cinemas.size(), 4U);
351                 BOOST_CHECK_EQUAL(cinemas[0].second.name, "Great");
352                 BOOST_CHECK_EQUAL(cinemas[1].second.name, "The ol' 1-seater");
353                 BOOST_CHECK_EQUAL(cinemas[2].second.name, "classy joint");
354                 BOOST_CHECK_EQUAL(cinemas[3].second.name, "stinking dump");
355         }
356 }
357
358
359 BOOST_AUTO_TEST_CASE(read_dkdm_recipients_xml_and_write_sqlite)
360 {
361         ConfigRestorer cr;
362
363         /* Set up a config with an XML cinemas file */
364         boost::filesystem::path dir = "build/test/read_dkdm_recipients_xml_and_write_sqlite";
365         boost::filesystem::remove_all(dir);
366         boost::filesystem::create_directories(dir);
367         boost::filesystem::create_directories(dir / "2.18");
368
369         boost::filesystem::copy_file("test/data/dkdm_recipients.xml", dir / "dkdm_recipients.xml");
370         boost::filesystem::copy_file("test/data/2.18.config.xml", dir / "2.18" / "config.xml");
371         {
372                 Editor editor(dir / "2.18" / "config.xml");
373                 editor.replace(
374                         "build/test/config_upgrade_test/dkdm_recipients.xml",
375                         boost::filesystem::canonical(dir / "dkdm_recipients.xml").string()
376                         );
377         }
378
379         Config::override_path = dir;
380         Config::drop();
381
382         /* This should make a sqlite3 file containing the recipients from dkdm_recipients.xml */
383         Config::instance();
384
385         {
386                 DKDMRecipientList test(dir / "dkdm_recipients.sqlite3");
387
388                 /* The detailed creation of sqlite3 from XML is tested in dkdm_recipient_list_test.cc */
389                 auto recipients = test.dkdm_recipients();
390                 BOOST_REQUIRE_EQUAL(recipients.size(), 2U);
391                 BOOST_CHECK_EQUAL(recipients[0].second.name, "Bob's Epics");
392                 BOOST_CHECK_EQUAL(recipients[1].second.name, "Sharon's Shorts");
393
394                 /* Add another recipient to the sqlite */
395                 test.add_dkdm_recipient({"Carl's Classics", "Oldies but goodies", {}, {}});
396         }
397
398         /* Reload the config; the old XML should not clobber the new sqlite3 */
399         Config::drop();
400         Config::instance();
401
402         {
403                 DKDMRecipientList test(dir / "dkdm_recipients.sqlite3");
404
405                 auto recipients = test.dkdm_recipients();
406                 BOOST_REQUIRE_EQUAL(recipients.size(), 3U);
407                 BOOST_CHECK_EQUAL(recipients[0].second.name, "Bob's Epics");
408                 BOOST_CHECK_EQUAL(recipients[1].second.name, "Carl's Classics");
409                 BOOST_CHECK_EQUAL(recipients[2].second.name, "Sharon's Shorts");
410         }
411 }
412
413
414 BOOST_AUTO_TEST_CASE(save_config_as_zip_test)
415 {
416         ConfigRestorer cr;
417
418         CinemaList cinemas;
419         cinemas.add_cinema({"My Great Cinema", {}, "", dcp::UTCOffset()});
420         DKDMRecipientList recipients;
421         recipients.add_dkdm_recipient({"Carl's Classics", "Oldies but goodies", {}, {}});
422
423         boost::filesystem::path const zip = "build/test/save.zip";
424         boost::system::error_code ec;
425         boost::filesystem::remove(zip, ec);
426         save_all_config_as_zip(zip);
427         Unzipper unzipper(zip);
428
429         BOOST_CHECK(unzipper.contains("config.xml"));
430         BOOST_CHECK(unzipper.contains("cinemas.sqlite3"));
431         BOOST_CHECK(unzipper.contains("dkdm_recipients.sqlite3"));
432 }
433
434
435 /** Load a config ZIP file, which contains an XML cinemas file, and ask to overwrite
436  *  the existing cinemas file that we had.
437  */
438 BOOST_AUTO_TEST_CASE(load_config_from_zip_with_only_xml_current)
439 {
440         ConfigRestorer cr;
441
442         auto cinemas_file = Config::instance()->cinemas_file();
443
444         boost::filesystem::path const zip = "build/test/load.zip";
445         boost::system::error_code ec;
446         boost::filesystem::remove(zip, ec);
447
448         Zipper zipper(zip);
449         zipper.add(
450                 "config.xml",
451                 boost::algorithm::replace_all_copy(
452                         dcp::file_to_string("test/data/2.18.config.xml"),
453                         "/home/realldoesnt/exist/this/path/is/nonsense.xml",
454                         ""
455                         )
456                 );
457
458         zipper.add("cinemas.xml", dcp::file_to_string("test/data/cinemas.xml"));
459         zipper.close();
460
461         Config::instance()->load_from_zip(zip, Config::CinemasAction::WRITE_TO_CURRENT_PATH);
462
463         CinemaList cinema_list(cinemas_file);
464         auto cinemas = cinema_list.cinemas();
465         BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
466         BOOST_CHECK_EQUAL(cinemas[0].second.name, "Great");
467         BOOST_CHECK_EQUAL(cinemas[1].second.name, "classy joint");
468         BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
469 }
470
471
472 /** Load a config ZIP file, which contains an XML cinemas file, and ask to write it to
473  *  the location specified by the zipped config.xml.
474  */
475 BOOST_AUTO_TEST_CASE(load_config_from_zip_with_only_xml_zip)
476 {
477         ConfigRestorer cr;
478
479         boost::filesystem::path const zip = "build/test/load.zip";
480         boost::system::error_code ec;
481         boost::filesystem::remove(zip, ec);
482
483         Zipper zipper(zip);
484         zipper.add(
485                 "config.xml",
486                 boost::algorithm::replace_all_copy(
487                         dcp::file_to_string("test/data/2.18.config.xml"),
488                         "/home/realldoesnt/exist/this/path/is/nonsense.xml",
489                         "build/test/hide/it/here/cinemas.sqlite3"
490                         )
491                 );
492
493         zipper.add("cinemas.xml", dcp::file_to_string("test/data/cinemas.xml"));
494         zipper.close();
495
496         Config::instance()->load_from_zip(zip, Config::CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG);
497
498         CinemaList cinema_list("build/test/hide/it/here/cinemas.sqlite3");
499         auto cinemas = cinema_list.cinemas();
500         BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
501         BOOST_CHECK_EQUAL(cinemas[0].second.name, "Great");
502         BOOST_CHECK_EQUAL(cinemas[1].second.name, "classy joint");
503         BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
504 }
505
506
507 /** Load a config ZIP file, which contains an XML cinemas file, and ask to ignore it */
508 BOOST_AUTO_TEST_CASE(load_config_from_zip_with_only_xml_ignore)
509 {
510         ConfigRestorer cr;
511
512         boost::filesystem::path const zip = "build/test/load.zip";
513         boost::system::error_code ec;
514         boost::filesystem::remove(zip, ec);
515
516         Zipper zipper(zip);
517         zipper.add(
518                 "config.xml",
519                 boost::algorithm::replace_all_copy(
520                         dcp::file_to_string("test/data/2.18.config.xml"),
521                         "/home/realldoesnt/exist/this/path/is/nonsense.xml",
522                         "build/test/hide/it/here/cinemas.sqlite3"
523                         )
524                 );
525
526         zipper.add("cinemas.xml", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Cinemas/>");
527         zipper.close();
528
529         Config::instance()->load_from_zip(zip, Config::CinemasAction::IGNORE);
530
531         CinemaList cinema_list("build/test/hide/it/here/cinemas.sqlite3");
532         auto cinemas = cinema_list.cinemas();
533         BOOST_CHECK(!cinemas.empty());
534 }