8 from functools import partial
10 import deltatar.deltatar as deltatar
11 import deltatar.crypto as crypto
12 import deltatar.tarfile as tarfile
14 from . import BaseTest
16 TEST_PASSWORD = "test1234"
19 VOLUME_OVERHEAD = 1.4 # account for tar overhead when fitting files into
20 # volumes; this is black magic
23 ###############################################################################
25 ###############################################################################
27 def flip_bits (fname, off, b=0x01, n=1):
29 Open file *fname* at offset *off*, replacing the next *n* bytes with
30 their values xor’ed with *b*.
32 fd = os.open (fname, os.O_RDWR)
35 pos = os.lseek (fd, off, os.SEEK_SET)
37 chunk = os.read (fd, n)
38 chunk = bytes (map (lambda v: v ^ b, chunk))
39 pos = os.lseek (fd, off, os.SEEK_SET)
46 def gz_header_size (fname, off=0):
48 Determine the length of the gzip header starting at *off* in file fname.
50 The header is variable length because it may contain the filename as NUL
53 # length so we need to determine where the actual payload starts
54 off = tarfile.GZ_HEADER_SIZE
55 fd = os.open (fname, os.O_RDONLY)
58 pos = os.lseek (fd, off, os.SEEK_SET)
60 while os.read (fd, 1)[0] != 0:
62 pos = os.lseek (fd, off, os.SEEK_SET)
70 def is_pdt_encrypted (fname):
72 Returns true if the file contains at least one PDT header plus enough
76 with open (fname, "rb") as st:
77 hdr = crypto.hdr_read_stream (st)
79 assert (len (st.read (siz)) == siz)
80 except Exception as exn:
85 ###############################################################################
86 ## corruption simulators ##
87 ###############################################################################
89 class UndefinedTest (Exception):
90 """No test available for the asked combination of parameters."""
92 def corrupt_header (_, fname, compress, encrypt):
94 Modify a significant byte in the object header of the format.
96 if encrypt is True: # damage GCM tag
97 flip_bits (fname, crypto.HDR_OFF_TAG + 1)
98 elif compress is True: # invalidate magic
100 else: # Fudge checksum. From tar(5):
102 # struct header_gnu_tar {
111 flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1)
114 def corrupt_ctsize (_, fname, compress, encrypt):
116 Blow up the size of an object so as to cause its apparent payload to leak
120 # damage lowest bit of second least significant byte of size field;
121 # this effectively sets the ciphertext size to 422, causing it to
122 # extend over the next object into the third one.
123 return flip_bits (fname, crypto.HDR_OFF_CTSIZE + 1, b=0x01)
124 raise UndefinedTest ("corrupt_ctsize %s %s %s" % (fname, compress, encrypt))
127 def corrupt_entire_header (_, fname, compress, encrypt):
129 Flip all bits in the first object header.
132 flip_bits (fname, 0, 0xff, crypto.PDTCRYPT_HDR_SIZE)
133 elif compress is True: # invalidate magic
134 flip_bits (fname, 0, 0xff, gz_header_size (fname))
136 flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE)
139 def corrupt_payload_start (_, fname, compress, encrypt):
141 Modify the byte following the object header structure of the format.
144 flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1)
145 elif compress is True:
146 flip_bits (fname, gz_header_size (fname) + 1)
148 flip_bits (fname, tarfile.BLOCKSIZE + 1)
151 def corrupt_leading_garbage (_, fname, compress, encrypt):
153 Prepend junk to file.
155 aname = os.path.abspath (fname)
156 infd = os.open (fname, os.O_RDONLY)
157 size = os.lseek (infd, 0, os.SEEK_END)
158 assert os.lseek (infd, 0, os.SEEK_SET) == 0
159 outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
160 stat.S_IRUSR | stat.S_IWUSR)
161 junk = os.urandom (42)
163 # write new file with garbage prepended
165 os.write (outfd, junk) # junk first
168 data = os.read (infd, TEST_BLOCKSIZE)
169 os.write (outfd, data)
172 assert os.lseek (outfd, 0, os.SEEK_CUR) == done
174 # close and free old file
178 # install the new file in its place, atomically
179 path = "/proc/self/fd/%d" % outfd
180 os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
184 def corrupt_trailing_data (_, fname, compress, encrypt):
186 Modify the byte following the object header structure of the format.
188 junk = os.urandom (42)
189 fd = os.open (fname, os.O_WRONLY | os.O_APPEND)
194 def corrupt_volume (_, fname, compress, encrypt):
196 Zero out an entire volume.
198 fd = os.open (fname, os.O_WRONLY)
199 size = os.lseek (fd, 0, os.SEEK_END)
200 assert os.lseek (fd, 0, os.SEEK_SET) == 0
201 zeros = bytes (b'\x00' * TEST_BLOCKSIZE)
203 todo = min (size, TEST_BLOCKSIZE)
204 os.write (fd, zeros [:todo])
209 def corrupt_hole (_, fname, compress, encrypt):
211 Cut file in three pieces, reassemble without the middle one.
213 aname = os.path.abspath (fname)
214 infd = os.open (fname, os.O_RDONLY)
215 size = os.lseek (infd, 0, os.SEEK_END)
216 assert os.lseek (infd, 0, os.SEEK_SET) == 0
217 assert size > 3 * TEST_BLOCKSIZE
218 hole = (size / 3, size * 2 / 3)
219 outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
220 stat.S_IRUSR | stat.S_IWUSR)
224 data = os.read (infd, TEST_BLOCKSIZE)
225 if done < hole [0] or hole [1] < done:
226 # only copy from outside hole
227 os.write (outfd, data)
233 path = "/proc/self/fd/%d" % outfd
234 os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
237 def immaculate (_, _fname, _compress, _encrypt):
243 ###############################################################################
245 ###############################################################################
247 class DefectiveTest (BaseTest):
249 Disaster recovery: restore corrupt backups.
254 FAILURES = 0 # files that could not be restored
255 MISMATCHES = 0 # files that were restored but corrupted
256 CORRUPT = corrupt_payload_start
258 MISSING = None # normally the number of failures
263 Create base test data
265 self.pwd = os.getcwd()
266 self.dst_path = "source_dir"
267 self.src_path = "%s2" % self.dst_path
270 os.system('rm -rf target_dir source_dir* backup_dir* huge')
271 os.makedirs (self.src_path)
275 self.hash [f] = self.create_file ("%s/%s"
276 % (self.src_path, f), 5 + i)
281 Remove temporal files created by unit tests and reset globals.
284 os.system("rm -rf source_dir source_dir2 backup_dir*")
288 def default_volume_name (backup_file, _x, _y, n, *a, **kwa):
289 return backup_file % n
291 def gen_file_names (self, comp, pw):
292 bak_path = "backup_dir"
293 backup_file = "the_full_backup_%0.2d.tar"
294 backup_full = ("%s/%s" % (bak_path, backup_file)) % 0
295 index_file = "the_full_index"
297 if self.COMPRESSION is not None:
302 if self.PASSWORD is not None:
303 backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION)
304 backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION)
305 index_file = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION)
307 return bak_path, backup_file, backup_full, index_file
310 def gen_multivol (self, nvol):
311 # add n files for one nth the volume size each, corrected
312 # for metadata and tar block overhead
313 fsiz = int ( ( TEST_VOLSIZ
314 / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
316 fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
317 for i in range (fcnt):
318 nvol, invol = divmod(i, TEST_FILESPERVOL)
319 f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
320 self.hash [f] = self.create_file ("%s/%s"
321 % (self.src_path, f),
326 class RecoverTest (DefectiveTest):
328 Recover: restore corrupt backups from index file information.
331 def test_recover_corrupt (self):
333 Perform various damaging actions that cause unreadable objects.
335 Expects the extraction to fail in normal mode. With disaster recovery,
336 extraction must succeed, and exactly one file must be missing.
338 mode = self.COMPRESSION or "#"
339 bak_path, backup_file, backup_full, index_file = \
340 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
343 self.gen_multivol (self.VOLUMES)
345 vname = partial (self.default_volume_name, backup_file)
346 dtar = deltatar.DeltaTar (mode=mode,
348 password=self.PASSWORD,
349 index_name_func=lambda _: index_file,
350 volume_name_func=vname)
352 dtar.create_full_backup \
353 (source_path=self.src_path, backup_path=bak_path,
356 if self.PASSWORD is not None:
357 # ensure all files are at least superficially in PDT format
358 for f in os.listdir (bak_path):
359 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
361 # first restore must succeed
362 dtar.restore_backup(target_path=self.dst_path,
363 backup_indexes_paths=[
364 "%s/%s" % (bak_path, index_file)
366 for key, value in self.hash.items ():
367 f = "%s/%s" % (self.dst_path, key)
368 assert os.path.exists (f)
369 assert value == self.md5sum (f)
370 shutil.rmtree (self.dst_path)
371 shutil.rmtree (self.src_path)
373 self.CORRUPT (backup_full,
374 self.COMPRESSION is not None,
375 self.PASSWORD is not None)
377 # normal restore must fail
379 dtar.restore_backup(target_path=self.dst_path,
380 backup_tar_path=backup_full)
381 except tarfile.CompressionError:
382 if self.PASSWORD is not None or self.COMPRESSION is not None:
386 except tarfile.ReadError:
387 # can happen with all three modes
389 except tarfile.DecryptionError:
390 if self.PASSWORD is not None:
395 os.chdir (self.pwd) # not restored due to the error above
396 # but recover will succeed
397 failed = dtar.recover_backup(target_path=self.dst_path,
398 backup_indexes_paths=[
399 "%s/%s" % (bak_path, index_file)
402 assert len (failed) == self.FAILURES
404 # with one file missing
407 for key, value in self.hash.items ():
408 kkey = "%s/%s" % (self.dst_path, key)
409 if os.path.exists (kkey):
410 if value != self.md5sum (kkey):
411 mismatch.append (key)
415 # usually, an object whose extraction fails will not be found on
416 # disk afterwards so the number of failures equals that of missing
417 # files. however, some modes will create partial files for objects
418 # spanning multiple volumes that contain the parts whose checksums
420 assert len (missing) == (self.MISSING if self.MISSING is not None
422 assert len (mismatch) == self.MISMATCHES
424 shutil.rmtree (self.dst_path)
427 class RescueTest (DefectiveTest):
429 Rescue: restore corrupt backups from backup set that is damaged to a degree
430 that the index file is worthless.
433 def test_rescue_corrupt (self):
435 Perform various damaging actions that cause unreadable objects, then
436 attempt to extract objects regardless.
438 mode = self.COMPRESSION or "#"
439 bak_path, backup_file, backup_full, index_file = \
440 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
443 self.gen_multivol (self.VOLUMES)
445 vname = partial (self.default_volume_name, backup_file)
446 dtar = deltatar.DeltaTar (mode=mode,
448 password=self.PASSWORD,
449 index_name_func=lambda _: index_file,
450 volume_name_func=vname)
452 dtar.create_full_backup \
453 (source_path=self.src_path, backup_path=bak_path,
456 if self.PASSWORD is not None:
457 # ensure all files are at least superficially in PDT format
458 for f in os.listdir (bak_path):
459 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
461 # first restore must succeed
462 dtar.restore_backup(target_path=self.dst_path,
463 backup_indexes_paths=[
464 "%s/%s" % (bak_path, index_file)
466 for key, value in self.hash.items ():
467 f = "%s/%s" % (self.dst_path, key)
468 assert os.path.exists (f)
469 assert value == self.md5sum (f)
470 shutil.rmtree (self.dst_path)
471 shutil.rmtree (self.src_path)
473 self.CORRUPT (backup_full,
474 self.COMPRESSION is not None,
475 self.PASSWORD is not None)
477 # normal restore must fail
479 dtar.restore_backup(target_path=self.dst_path,
480 backup_tar_path=backup_full)
481 except tarfile.CompressionError:
482 if self.PASSWORD is not None or self.COMPRESSION is not None:
486 except tarfile.ReadError:
487 # can happen with all three modes
489 except tarfile.DecryptionError:
490 if self.PASSWORD is not None:
495 os.chdir (self.pwd) # not restored due to the error above
496 # but recover will succeed
497 failed = dtar.rescue_backup(target_path=self.dst_path,
498 backup_tar_path=backup_full)
499 # with one file missing
502 for key, value in self.hash.items ():
503 kkey = "%s/%s" % (self.dst_path, key)
504 if os.path.exists (kkey):
505 if value != self.md5sum (kkey):
506 mismatch.append (key)
510 assert len (failed) == self.FAILURES
511 assert len (missing) == (self.MISSING if self.MISSING is not None
513 assert len (mismatch) == self.MISMATCHES
515 shutil.rmtree (self.dst_path)
518 class GenIndexTest (DefectiveTest):
520 Deducing an index for a backup with tarfile.
523 def test_gen_index (self):
525 Create backup, leave it unharmed, then generate an index.
527 mode = self.COMPRESSION or "#"
528 bak_path, backup_file, backup_full, index_file = \
529 self.gen_file_names (self.COMPRESSION, self.PASSWORD)
532 self.gen_multivol (self.VOLUMES)
534 vname = partial (self.default_volume_name, backup_file)
535 dtar = deltatar.DeltaTar (mode=mode,
537 password=self.PASSWORD,
538 index_name_func=lambda _: index_file,
539 volume_name_func=vname)
541 dtar.create_full_backup \
542 (source_path=self.src_path, backup_path=bak_path,
545 def gen_volume_name (nvol):
546 return os.path.join (bak_path, vname (backup_full, True, nvol))
548 psidx = tarfile.gen_rescue_index (gen_volume_name,
550 password=self.PASSWORD)
552 # correct for objects spanning volumes: these are treated as separate
554 assert len (psidx) - self.VOLUMES + 1 == len (self.hash)
557 ###############################################################################
559 ###############################################################################
561 class RecoverCorruptPayloadTestBase (RecoverTest):
564 FAILURES = 0 # tarfile will restore but corrupted, as
565 MISMATCHES = 1 # revealed by the hash
567 class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
570 class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
574 class RecoverCorruptPayloadGZTestBase (RecoverTest):
580 class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
583 class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
587 class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
589 PASSWORD = TEST_PASSWORD
593 class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
596 class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
600 class RecoverCorruptHeaderTestBase (RecoverTest):
604 CORRUPT = corrupt_header
607 class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
610 class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
614 class RecoverCorruptHeaderGZTestBase (RecoverTest):
618 CORRUPT = corrupt_header
621 class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
624 class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
628 class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
630 PASSWORD = TEST_PASSWORD
632 CORRUPT = corrupt_header
635 class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
638 class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
642 class RecoverCorruptEntireHeaderTestBase (RecoverTest):
646 CORRUPT = corrupt_entire_header
649 class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
652 class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
656 class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
660 CORRUPT = corrupt_entire_header
663 class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
666 class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
670 class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
672 PASSWORD = TEST_PASSWORD
674 CORRUPT = corrupt_entire_header
677 class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
680 class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
684 class RecoverCorruptTrailingDataTestBase (RecoverTest):
685 # plain Tar is indifferent against traling data and the results
690 CORRUPT = corrupt_trailing_data
693 class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
696 class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
697 # the last object in first archive has extra bytes somewhere in the
698 # middle because tar itself performs no data checksumming.
703 class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
704 # reading past the final object will cause decompression failure;
705 # all objects except for the last survive unharmed though
709 CORRUPT = corrupt_trailing_data
712 class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
715 class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
717 # the last file of the first volume will only contain the data of the
718 # second part which is contained in the second volume. this happens
719 # because the CRC32 is wrong for the first part so it gets discarded, then
720 # the object is recreated from the first header of the second volume,
721 # containing only the remainder of the data.
726 class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
728 PASSWORD = TEST_PASSWORD
730 CORRUPT = corrupt_trailing_data
733 class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
736 class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
740 class RecoverCorruptVolumeBaseTest (RecoverTest):
744 CORRUPT = corrupt_volume
747 class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
750 class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
753 class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
755 PASSWORD = TEST_PASSWORD
758 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
759 class RecoverCorruptHoleBaseTest (RecoverTest):
761 Cut bytes from the middle of a volume.
763 Index-based recovery works only up to the hole.
768 CORRUPT = corrupt_hole
769 VOLUMES = 2 # request two vols to swell up the first one
772 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
773 class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
776 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
777 class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
781 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
782 class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
784 PASSWORD = TEST_PASSWORD
787 ###############################################################################
789 ###############################################################################
791 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
792 class RescueCorruptHoleBaseTest (RescueTest):
794 Cut bytes from the middle of a volume.
799 CORRUPT = corrupt_hole
800 VOLUMES = 2 # request two vols to swell up the first one
801 MISMATCHES = 2 # intersected by hole
802 MISSING = 1 # excised by hole
804 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
805 class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
808 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
809 class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
811 # the decompressor explodes in our face processing the first dummy, nothing
812 # we can do to recover
815 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
816 class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
818 PASSWORD = TEST_PASSWORD
819 # again, ignoring the crypto errors yields a bad zlib stream causing the
820 # decompressor to abort where the hole begins; the file is extracted up
821 # to this point though
825 class RescueCorruptHeaderCTSizeGZAESTest (RescueTest):
827 PASSWORD = TEST_PASSWORD
829 CORRUPT = corrupt_ctsize
833 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
834 class RescueCorruptLeadingGarbageTestBase (RescueTest):
835 # plain Tar is indifferent against traling data and the results
840 CORRUPT = corrupt_leading_garbage
843 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
844 class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase):
847 @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library")
848 class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase):
849 # the last object in first archive has extra bytes somewhere in the
850 # middle because tar itself performs no data checksumming.
855 ###############################################################################
857 ###############################################################################
859 class GenIndexIntactBaseTest (GenIndexTest):
869 class GenIndexIntactSingleTest (GenIndexIntactBaseTest):
872 class GenIndexIntactSingleGZTest (GenIndexIntactBaseTest):
876 class GenIndexIntactSingleGZAESTest (GenIndexIntactBaseTest):
878 PASSWORD = TEST_PASSWORD
881 class GenIndexIntactMultiTest (GenIndexIntactBaseTest):
885 class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest):
890 class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest):
893 PASSWORD = TEST_PASSWORD
897 class GenIndexCorruptHoleBaseTest (GenIndexTest):
899 Recreate index from file with hole.
904 CORRUPT = corrupt_hole
908 class GenIndexCorruptHoleTest (GenIndexCorruptHoleBaseTest):
911 class GenIndexCorruptHoleGZTest (GenIndexCorruptHoleBaseTest):
915 class GenIndexCorruptHoleGZAESTest (GenIndexCorruptHoleBaseTest):
917 PASSWORD = TEST_PASSWORD
922 class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest):
924 Recreate index from file with hole.
929 CORRUPT = corrupt_entire_header
933 class GenIndexCorruptEntireHeaderTest (GenIndexCorruptEntireHeaderBaseTest):
936 class GenIndexCorruptEntireHeaderGZTest (GenIndexCorruptEntireHeaderBaseTest):
940 class GenIndexCorruptEntireHeaderGZAESTest (GenIndexCorruptEntireHeaderBaseTest):
942 PASSWORD = TEST_PASSWORD