Пример #1
0
class TestContactList(TFCTestCase):
    def setUp(self):
        """Pre-test actions."""
        self.unit_test_dir = cd_unit_test()
        self.master_key = MasterKey()
        self.settings = Settings()
        self.file_name = f'{DIR_USER_DATA}{self.settings.software_operation}_contacts'
        self.contact_list = ContactList(self.master_key, self.settings)
        self.full_contact_list = [
            'Alice', 'Bob', 'Charlie', 'David', 'Eric', LOCAL_ID
        ]
        self.contact_list.contacts = list(
            map(create_contact, self.full_contact_list))
        self.real_contact_list = self.full_contact_list[:]
        self.real_contact_list.remove(LOCAL_ID)

    def tearDown(self):
        """Post-test actions."""
        cleanup(self.unit_test_dir)

    def test_contact_list_iterates_over_contact_objects(self):
        for c in self.contact_list:
            self.assertIsInstance(c, Contact)

    def test_len_returns_the_number_of_contacts_and_excludes_the_local_key(
            self):
        self.assertEqual(len(self.contact_list), len(self.real_contact_list))

    def test_storing_and_loading_of_contacts(self):
        # Test store
        self.contact_list.store_contacts()
        self.assertEqual(
            os.path.getsize(self.file_name), XCHACHA20_NONCE_LENGTH +
            (self.settings.max_number_of_contacts + 1) * CONTACT_LENGTH +
            POLY1305_TAG_LENGTH)

        # Test load
        contact_list2 = ContactList(self.master_key, self.settings)
        self.assertEqual(len(contact_list2), len(self.real_contact_list))
        self.assertEqual(len(contact_list2.contacts),
                         len(self.full_contact_list))
        for c in contact_list2:
            self.assertIsInstance(c, Contact)

    def test_invalid_content_raises_critical_error(self):
        # Setup
        invalid_data = b'a'
        pt_bytes = b''.join([
            c.serialize_c() for c in self.contact_list.contacts +
            self.contact_list._dummy_contacts()
        ])
        ct_bytes = encrypt_and_sign(pt_bytes + invalid_data,
                                    self.master_key.master_key)

        ensure_dir(DIR_USER_DATA)
        with open(self.file_name, 'wb+') as f:
            f.write(ct_bytes)

        # Test
        with self.assertRaises(SystemExit):
            ContactList(self.master_key, self.settings)

    def test_load_of_modified_database_raises_critical_error(self):
        self.contact_list.store_contacts()

        # Test reading works normally
        self.assertIsInstance(ContactList(self.master_key, self.settings),
                              ContactList)

        # Test loading of tampered database raises CriticalError
        tamper_file(self.file_name, tamper_size=1)
        with self.assertRaises(SystemExit):
            ContactList(self.master_key, self.settings)

    def test_generate_dummy_contact(self):
        dummy_contact = ContactList.generate_dummy_contact()
        self.assertIsInstance(dummy_contact, Contact)
        self.assertEqual(len(dummy_contact.serialize_c()), CONTACT_LENGTH)

    def test_dummy_contacts(self):
        dummies = self.contact_list._dummy_contacts()
        self.assertEqual(
            len(dummies),
            self.settings.max_number_of_contacts - len(self.real_contact_list))
        for c in dummies:
            self.assertIsInstance(c, Contact)

    def test_add_contact(self):
        tx_fingerprint = FINGERPRINT_LENGTH * b'\x03'
        rx_fingerprint = FINGERPRINT_LENGTH * b'\x04'

        self.assertIsNone(
            self.contact_list.add_contact(
                nick_to_pub_key('Faye'), 'Faye', tx_fingerprint,
                rx_fingerprint, KEX_STATUS_UNVERIFIED,
                self.settings.log_messages_by_default,
                self.settings.accept_files_by_default,
                self.settings.show_notifications_by_default))

        # Test new contact was stored by loading
        # the database from file to another object
        contact_list2 = ContactList(MasterKey(), Settings())
        faye = contact_list2.get_contact_by_pub_key(nick_to_pub_key('Faye'))

        self.assertEqual(len(self.contact_list),
                         len(self.real_contact_list) + 1)
        self.assertIsInstance(faye, Contact)

        self.assertEqual(faye.tx_fingerprint, tx_fingerprint)
        self.assertEqual(faye.rx_fingerprint, rx_fingerprint)
        self.assertEqual(faye.kex_status, KEX_STATUS_UNVERIFIED)

        self.assertEqual(faye.log_messages,
                         self.settings.log_messages_by_default)
        self.assertEqual(faye.file_reception,
                         self.settings.accept_files_by_default)
        self.assertEqual(faye.notifications,
                         self.settings.show_notifications_by_default)

    def test_add_contact_that_replaces_an_existing_contact(self):
        alice = self.contact_list.get_contact_by_pub_key(
            nick_to_pub_key('Alice'))
        new_nick = 'Alice2'
        new_tx_fingerprint = FINGERPRINT_LENGTH * b'\x03'
        new_rx_fingerprint = FINGERPRINT_LENGTH * b'\x04'

        # Verify that existing nick, kex status and fingerprints are
        # different from those that will replace the existing data
        self.assertNotEqual(alice.nick, new_nick)
        self.assertNotEqual(alice.tx_fingerprint, new_tx_fingerprint)
        self.assertNotEqual(alice.rx_fingerprint, new_rx_fingerprint)
        self.assertNotEqual(alice.kex_status, KEX_STATUS_UNVERIFIED)

        # Make sure each contact setting is opposite from default value
        alice.log_messages = not self.settings.log_messages_by_default
        alice.file_reception = not self.settings.accept_files_by_default
        alice.notifications = not self.settings.show_notifications_by_default

        # Replace the existing contact
        self.assertIsNone(
            self.contact_list.add_contact(
                nick_to_pub_key('Alice'), new_nick, new_tx_fingerprint,
                new_rx_fingerprint, KEX_STATUS_UNVERIFIED,
                self.settings.log_messages_by_default,
                self.settings.accept_files_by_default,
                self.settings.show_notifications_by_default))

        # Load database to another object from
        # file to verify new contact was stored
        contact_list2 = ContactList(MasterKey(), Settings())
        alice = contact_list2.get_contact_by_pub_key(nick_to_pub_key('Alice'))

        # Verify the content of loaded data
        self.assertEqual(len(contact_list2), len(self.real_contact_list))
        self.assertIsInstance(alice, Contact)

        # Test replaced contact replaced nick, fingerprints and kex status
        self.assertEqual(alice.nick, new_nick)
        self.assertEqual(alice.tx_fingerprint, new_tx_fingerprint)
        self.assertEqual(alice.rx_fingerprint, new_rx_fingerprint)
        self.assertEqual(alice.kex_status, KEX_STATUS_UNVERIFIED)

        # Test replaced contact kept settings set
        # to be opposite from default settings
        self.assertNotEqual(alice.log_messages,
                            self.settings.log_messages_by_default)
        self.assertNotEqual(alice.file_reception,
                            self.settings.accept_files_by_default)
        self.assertNotEqual(alice.notifications,
                            self.settings.show_notifications_by_default)

    def test_remove_contact_by_pub_key(self):
        # Verify both contacts exist
        self.assertTrue(self.contact_list.has_pub_key(nick_to_pub_key('Bob')))
        self.assertTrue(
            self.contact_list.has_pub_key(nick_to_pub_key('Charlie')))

        self.assertIsNone(
            self.contact_list.remove_contact_by_pub_key(
                nick_to_pub_key('Bob')))
        self.assertFalse(self.contact_list.has_pub_key(nick_to_pub_key('Bob')))
        self.assertTrue(
            self.contact_list.has_pub_key(nick_to_pub_key('Charlie')))

    def test_remove_contact_by_address_or_nick(self):
        # Verify both contacts exist
        self.assertTrue(self.contact_list.has_pub_key(nick_to_pub_key('Bob')))
        self.assertTrue(
            self.contact_list.has_pub_key(nick_to_pub_key('Charlie')))

        # Test removal with address
        self.assertIsNone(
            self.contact_list.remove_contact_by_address_or_nick(
                nick_to_onion_address('Bob')))
        self.assertFalse(self.contact_list.has_pub_key(nick_to_pub_key('Bob')))
        self.assertTrue(
            self.contact_list.has_pub_key(nick_to_pub_key('Charlie')))

        # Test removal with nick
        self.assertIsNone(
            self.contact_list.remove_contact_by_address_or_nick('Charlie'))
        self.assertFalse(self.contact_list.has_pub_key(nick_to_pub_key('Bob')))
        self.assertFalse(
            self.contact_list.has_pub_key(nick_to_pub_key('Charlie')))

    def test_get_contact_by_pub_key(self):
        self.assertIs(
            self.contact_list.get_contact_by_pub_key(nick_to_pub_key('Bob')),
            self.contact_list.get_contact_by_address_or_nick('Bob'))

    def test_get_contact_by_address_or_nick_returns_the_same_contact_object_with_address_and_nick(
            self):
        for selector in [nick_to_onion_address('Bob'), 'Bob']:
            self.assertIsInstance(
                self.contact_list.get_contact_by_address_or_nick(selector),
                Contact)

        self.assertIs(
            self.contact_list.get_contact_by_address_or_nick('Bob'),
            self.contact_list.get_contact_by_address_or_nick(
                nick_to_onion_address('Bob')))

    def test_get_list_of_contacts(self):
        self.assertEqual(len(self.contact_list.get_list_of_contacts()),
                         len(self.real_contact_list))
        for c in self.contact_list.get_list_of_contacts():
            self.assertIsInstance(c, Contact)

    def test_get_list_of_addresses(self):
        self.assertEqual(self.contact_list.get_list_of_addresses(), [
            nick_to_onion_address('Alice'),
            nick_to_onion_address('Bob'),
            nick_to_onion_address('Charlie'),
            nick_to_onion_address('David'),
            nick_to_onion_address('Eric')
        ])

    def test_get_list_of_nicks(self):
        self.assertEqual(self.contact_list.get_list_of_nicks(),
                         ['Alice', 'Bob', 'Charlie', 'David', 'Eric'])

    def test_get_list_of_pub_keys(self):
        self.assertEqual(self.contact_list.get_list_of_pub_keys(), [
            nick_to_pub_key('Alice'),
            nick_to_pub_key('Bob'),
            nick_to_pub_key('Charlie'),
            nick_to_pub_key('David'),
            nick_to_pub_key('Eric')
        ])

    def test_get_list_of_pending_pub_keys(self):
        # Set key exchange statuses to pending
        for nick in ['Alice', 'Bob']:
            contact = self.contact_list.get_contact_by_address_or_nick(nick)
            contact.kex_status = KEX_STATUS_PENDING

        # Test pending contacts are returned
        self.assertEqual(self.contact_list.get_list_of_pending_pub_keys(),
                         [nick_to_pub_key('Alice'),
                          nick_to_pub_key('Bob')])

    def test_get_list_of_existing_pub_keys(self):
        self.contact_list.get_contact_by_address_or_nick(
            'Alice').kex_status = KEX_STATUS_UNVERIFIED
        self.contact_list.get_contact_by_address_or_nick(
            'Bob').kex_status = KEX_STATUS_VERIFIED
        self.contact_list.get_contact_by_address_or_nick(
            'Charlie').kex_status = KEX_STATUS_HAS_RX_PSK
        self.contact_list.get_contact_by_address_or_nick(
            'David').kex_status = KEX_STATUS_NO_RX_PSK
        self.contact_list.get_contact_by_address_or_nick(
            'Eric').kex_status = KEX_STATUS_PENDING

        self.assertEqual(self.contact_list.get_list_of_existing_pub_keys(), [
            nick_to_pub_key('Alice'),
            nick_to_pub_key('Bob'),
            nick_to_pub_key('Charlie'),
            nick_to_pub_key('David')
        ])

    def test_contact_selectors(self):
        self.assertEqual(self.contact_list.contact_selectors(), [
            nick_to_onion_address('Alice'),
            nick_to_onion_address('Bob'),
            nick_to_onion_address('Charlie'),
            nick_to_onion_address('David'),
            nick_to_onion_address('Eric'), 'Alice', 'Bob', 'Charlie', 'David',
            'Eric'
        ])

    def test_has_contacts(self):
        self.assertTrue(self.contact_list.has_contacts())
        self.contact_list.contacts = []
        self.assertFalse(self.contact_list.has_contacts())

    def test_has_only_pending_contacts(self):
        # Change all to pending
        for contact in self.contact_list.get_list_of_contacts():
            contact.kex_status = KEX_STATUS_PENDING
        self.assertTrue(self.contact_list.has_only_pending_contacts())

        # Change one from pending
        alice = self.contact_list.get_contact_by_address_or_nick('Alice')
        alice.kex_status = KEX_STATUS_UNVERIFIED
        self.assertFalse(self.contact_list.has_only_pending_contacts())

    def test_has_pub_key(self):
        self.contact_list.contacts = []
        self.assertFalse(self.contact_list.has_pub_key(nick_to_pub_key('Bob')))
        self.assertFalse(self.contact_list.has_pub_key(nick_to_pub_key('Bob')))

        self.contact_list.contacts = list(
            map(create_contact, ['Bob', 'Charlie']))
        self.assertTrue(self.contact_list.has_pub_key(nick_to_pub_key('Bob')))
        self.assertTrue(
            self.contact_list.has_pub_key(nick_to_pub_key('Charlie')))

    def test_has_local_contact(self):
        self.contact_list.contacts = []
        self.assertFalse(self.contact_list.has_local_contact())

        self.contact_list.contacts = [create_contact(LOCAL_ID)]
        self.assertTrue(self.contact_list.has_local_contact())

    def test_print_contacts(self):
        self.contact_list.contacts.append(create_contact(LOCAL_ID))
        self.contact_list.get_contact_by_pub_key(
            nick_to_pub_key('Alice')).log_messages = False
        self.contact_list.get_contact_by_pub_key(
            nick_to_pub_key('Alice')).kex_status = KEX_STATUS_PENDING
        self.contact_list.get_contact_by_pub_key(
            nick_to_pub_key('Bob')).notifications = False
        self.contact_list.get_contact_by_pub_key(
            nick_to_pub_key('Charlie')).kex_status = KEX_STATUS_UNVERIFIED
        self.contact_list.get_contact_by_pub_key(
            nick_to_pub_key('Bob')).file_reception = False
        self.contact_list.get_contact_by_pub_key(
            nick_to_pub_key('Bob')).kex_status = KEX_STATUS_VERIFIED
        self.contact_list.get_contact_by_pub_key(nick_to_pub_key(
            'David')).rx_fingerprint = bytes(FINGERPRINT_LENGTH)
        self.contact_list.get_contact_by_pub_key(
            nick_to_pub_key('David')).kex_status = bytes(KEX_STATUS_NO_RX_PSK)
        self.assert_prints(
            CLEAR_ENTIRE_SCREEN + CURSOR_LEFT_UP_CORNER + f"""\

Contact    Account    Logging    Notify    Files     Key Ex
────────────────────────────────────────────────────────────────────────────────
Alice      hpcra      No         Yes       Accept    {ECDHE} (Pending)
Bob        zwp3d      Yes        No        Reject    {ECDHE} (Verified)
Charlie    n2a3c      Yes        Yes       Accept    {ECDHE} (Unverified)
David      u22uy      Yes        Yes       Accept    {PSK}  (No contact key)
Eric       jszzy      Yes        Yes       Accept    {ECDHE} (Verified)


""", self.contact_list.print_contacts)
Пример #2
0
class TestGroupList(TFCTestCase):
    def setUp(self):
        self.unittest_dir = cd_unittest()
        self.master_key = MasterKey()
        self.settings = Settings()
        self.file_name = f'{DIR_USER_DATA}{self.settings.software_operation}_groups'
        self.contact_list = ContactList(self.master_key, self.settings)
        self.group_list = GroupList(self.master_key, self.settings,
                                    self.contact_list)
        self.nicks = [
            'Alice', 'Bob', 'Charlie', 'David', 'Eric', 'Fido', 'Guido',
            'Heidi', 'Ivan', 'Joana', 'Karol'
        ]
        self.group_names = [
            'test_group_1', 'test_group_2', 'test_group_3', 'test_group_4',
            'test_group_5', 'test_group_6', 'test_group_7', 'test_group_8',
            'test_group_9', 'test_group_10', 'test_group_11'
        ]
        members = list(map(create_contact, self.nicks))

        self.contact_list.contacts = members

        self.group_list.groups = \
            [Group(name         =name,
                   group_id     =group_name_to_group_id(name),
                   log_messages =False,
                   notifications=False,
                   members      =members,
                   settings     =self.settings,
                   store_groups =self.group_list.store_groups)
             for name in self.group_names]

        self.single_member_data_len = (
            GROUP_STATIC_LENGTH + self.settings.max_number_of_group_members *
            ONION_SERVICE_PUBLIC_KEY_LENGTH)

    def tearDown(self):
        cleanup(self.unittest_dir)

    def test_group_list_iterates_over_group_objects(self):
        for g in self.group_list:
            self.assertIsInstance(g, Group)

    def test_len_returns_the_number_of_groups(self):
        self.assertEqual(len(self.group_list), len(self.group_names))

    def test_storing_and_loading_of_groups(self):
        self.group_list.store_groups()

        self.assertTrue(os.path.isfile(self.file_name))
        self.assertEqual(
            os.path.getsize(self.file_name),
            XCHACHA20_NONCE_LENGTH + GROUP_DB_HEADER_LENGTH +
            self.settings.max_number_of_groups * self.single_member_data_len +
            POLY1305_TAG_LENGTH)

        # Reduce setting values from 20 to 10
        self.settings.max_number_of_groups = 10
        self.settings.max_number_of_group_members = 10

        group_list2 = GroupList(self.master_key, self.settings,
                                self.contact_list)
        self.assertEqual(len(group_list2), 11)

        # Check that `_load_groups()` increased setting values back to 20 so it fits the 11 groups
        self.assertEqual(self.settings.max_number_of_groups, 20)
        self.assertEqual(self.settings.max_number_of_group_members, 20)

        # Check that removed contact from contact list updates group
        self.contact_list.remove_contact_by_address_or_nick('Alice')
        group_list3 = GroupList(self.master_key, self.settings,
                                self.contact_list)
        self.assertEqual(len(group_list3.get_group('test_group_1').members),
                         10)

    def test_invalid_content_raises_critical_error(self):
        # Setup
        invalid_data = b'a'
        pt_bytes = self.group_list._generate_group_db_header()
        pt_bytes += b''.join([
            g.serialize_g()
            for g in (self.group_list.groups + self.group_list._dummy_groups())
        ])
        ct_bytes = encrypt_and_sign(pt_bytes + invalid_data,
                                    self.master_key.master_key)

        ensure_dir(DIR_USER_DATA)
        with open(self.file_name, 'wb+') as f:
            f.write(ct_bytes)

        # Test
        with self.assertRaises(SystemExit):
            GroupList(self.master_key, self.settings, self.contact_list)

    def test_load_of_modified_database_raises_critical_error(self):
        self.group_list.store_groups()

        # Test reading works normally
        self.assertIsInstance(
            GroupList(self.master_key, self.settings, self.contact_list),
            GroupList)

        # Test loading of the tampered database raises CriticalError
        tamper_file(self.file_name, tamper_size=1)
        with self.assertRaises(SystemExit):
            GroupList(self.master_key, self.settings, self.contact_list)

    def test_check_db_settings(self):
        self.assertFalse(
            self.group_list._check_db_settings(
                number_of_actual_groups=self.settings.max_number_of_groups,
                members_in_largest_group=self.settings.
                max_number_of_group_members))

        self.assertTrue(
            self.group_list._check_db_settings(
                number_of_actual_groups=self.settings.max_number_of_groups + 1,
                members_in_largest_group=self.settings.
                max_number_of_group_members))

        self.assertTrue(
            self.group_list._check_db_settings(
                number_of_actual_groups=self.settings.max_number_of_groups,
                members_in_largest_group=self.settings.
                max_number_of_group_members + 1))

    def test_generate_group_db_header(self):
        header = self.group_list._generate_group_db_header()
        self.assertEqual(len(header), GROUP_DB_HEADER_LENGTH)
        self.assertIsInstance(header, bytes)

    def test_generate_dummy_group(self):
        dummy_group = self.group_list._generate_dummy_group()
        self.assertIsInstance(dummy_group, Group)
        self.assertEqual(len(dummy_group.serialize_g()),
                         self.single_member_data_len)

    def test_dummy_groups(self):
        dummies = self.group_list._dummy_groups()
        self.assertEqual(
            len(dummies),
            self.settings.max_number_of_contacts - len(self.nicks))
        for g in dummies:
            self.assertIsInstance(g, Group)

    def test_add_group(self):
        members = [create_contact('Laura')]
        self.group_list.add_group('test_group_12', bytes(GROUP_ID_LENGTH),
                                  False, False, members)
        self.group_list.add_group('test_group_12', bytes(GROUP_ID_LENGTH),
                                  False, True, members)
        self.assertTrue(
            self.group_list.get_group('test_group_12').notifications)
        self.assertEqual(len(self.group_list), len(self.group_names) + 1)

    def test_remove_group_by_name(self):
        self.assertEqual(len(self.group_list), len(self.group_names))

        # Remove non-existing group
        self.assertIsNone(
            self.group_list.remove_group_by_name('test_group_12'))
        self.assertEqual(len(self.group_list), len(self.group_names))

        # Remove existing group
        self.assertIsNone(
            self.group_list.remove_group_by_name('test_group_11'))
        self.assertEqual(len(self.group_list), len(self.group_names) - 1)

    def test_remove_group_by_id(self):
        self.assertEqual(len(self.group_list), len(self.group_names))

        # Remove non-existing group
        self.assertIsNone(
            self.group_list.remove_group_by_id(
                group_name_to_group_id('test_group_12')))
        self.assertEqual(len(self.group_list), len(self.group_names))

        # Remove existing group
        self.assertIsNone(
            self.group_list.remove_group_by_id(
                group_name_to_group_id('test_group_11')))
        self.assertEqual(len(self.group_list), len(self.group_names) - 1)

    def test_get_group(self):
        self.assertEqual(
            self.group_list.get_group('test_group_3').name, 'test_group_3')

    def test_get_group_by_id(self):
        members = [create_contact('Laura')]
        group_id = os.urandom(GROUP_ID_LENGTH)
        self.group_list.add_group('test_group_12', group_id, False, False,
                                  members)
        self.assertEqual(
            self.group_list.get_group_by_id(group_id).name, 'test_group_12')

    def test_get_list_of_group_names(self):
        self.assertEqual(self.group_list.get_list_of_group_names(),
                         self.group_names)

    def test_get_list_of_group_ids(self):
        self.assertEqual(self.group_list.get_list_of_group_ids(),
                         list(map(group_name_to_group_id, self.group_names)))

    def test_get_list_of_hr_group_ids(self):
        self.assertEqual(self.group_list.get_list_of_hr_group_ids(), [
            b58encode(gid)
            for gid in list(map(group_name_to_group_id, self.group_names))
        ])

    def test_get_group_members(self):
        members = self.group_list.get_group_members(
            group_name_to_group_id('test_group_1'))
        for c in members:
            self.assertIsInstance(c, Contact)

    def test_has_group(self):
        self.assertTrue(self.group_list.has_group('test_group_11'))
        self.assertFalse(self.group_list.has_group('test_group_12'))

    def test_has_group_id(self):
        members = [create_contact('Laura')]
        group_id = os.urandom(GROUP_ID_LENGTH)
        self.assertFalse(self.group_list.has_group_id(group_id))
        self.group_list.add_group('test_group_12', group_id, False, False,
                                  members)
        self.assertTrue(self.group_list.has_group_id(group_id))

    def test_largest_group(self):
        self.assertEqual(self.group_list.largest_group(), len(self.nicks))

    def test_print_group(self):
        self.group_list.get_group("test_group_1").name = "group"
        self.group_list.get_group("test_group_2").log_messages = True
        self.group_list.get_group("test_group_3").notifications = True
        self.group_list.get_group("test_group_4").log_messages = True
        self.group_list.get_group("test_group_4").notifications = True
        self.group_list.get_group("test_group_5").members = []
        self.group_list.get_group("test_group_6").members = list(
            map(create_contact,
                ['Alice', 'Bob', 'Charlie', 'David', 'Eric', 'Fido']))
        self.assert_prints(
            """\
Group            Group ID         Logging     Notify    Members
────────────────────────────────────────────────────────────────────────────────
group            2drs4c4VcDdrP    No          No        Alice, Bob, Charlie,
                                                        David, Eric, Fido,
                                                        Guido, Heidi, Ivan,
                                                        Joana, Karol

test_group_2     2dnGTyhkThmPi    Yes         No        Alice, Bob, Charlie,
                                                        David, Eric, Fido,
                                                        Guido, Heidi, Ivan,
                                                        Joana, Karol

test_group_3     2df7s3LZhwLDw    No          Yes       Alice, Bob, Charlie,
                                                        David, Eric, Fido,
                                                        Guido, Heidi, Ivan,
                                                        Joana, Karol

test_group_4     2djy3XwUQVR8q    Yes         Yes       Alice, Bob, Charlie,
                                                        David, Eric, Fido,
                                                        Guido, Heidi, Ivan,
                                                        Joana, Karol

test_group_5     2dvbcgnjiLLMo    No          No        <Empty group>

test_group_6     2dwBRWAqWKHWv    No          No        Alice, Bob, Charlie,
                                                        David, Eric, Fido

test_group_7     2eDPg5BAM6qF4    No          No        Alice, Bob, Charlie,
                                                        David, Eric, Fido,
                                                        Guido, Heidi, Ivan,
                                                        Joana, Karol

test_group_8     2dqdayy5TJKcf    No          No        Alice, Bob, Charlie,
                                                        David, Eric, Fido,
                                                        Guido, Heidi, Ivan,
                                                        Joana, Karol

test_group_9     2e45bLYvSX3C8    No          No        Alice, Bob, Charlie,
                                                        David, Eric, Fido,
                                                        Guido, Heidi, Ivan,
                                                        Joana, Karol

test_group_10    2dgkncX9xRibh    No          No        Alice, Bob, Charlie,
                                                        David, Eric, Fido,
                                                        Guido, Heidi, Ivan,
                                                        Joana, Karol

test_group_11    2e6vAGmHmSEEJ    No          No        Alice, Bob, Charlie,
                                                        David, Eric, Fido,
                                                        Guido, Heidi, Ivan,
                                                        Joana, Karol


""", self.group_list.print_groups)