#!/usr/share/ucs-test/runner python3
# -*- coding: utf-8 -*
## desc: memberOf replication tests
## tags:
##  - replication
##  - apptest
## roles:
##  - domaincontroller_master
##  - domaincontroller_backup
##  - domaincontroller_slave
## packages:
##  - univention-config
##  - univention-directory-manager-tools
##  - ldap-utils
## bugs:
##  - 46590
## exposure: dangerous

"""
The test checks, if the memberOf attribute is correctly set in all known test cases.
On DC Backup + DC Slave systems, there were missing memberOf values at user objects
if the listener was offline for some time OR there was a racing condition.

The test script runs through several scenarios (a test function for each scenario)
and tests with with_listener=False to trigger the racing condition (see bug #46590
for details) and with with_listener=True to make sure, that the usual scenario is
not affected.

Due to the inability of the S4 connector to handle, to handle group changes in diff mode
and the ucs-test framework to wait on all systems until all sync steps are done, the
test will be skipped for now on all system with samba4 installed but deactivated s4-connector.
Otherwise the test would be flaky.
"""


from __future__ import absolute_import
from __future__ import print_function
from univention.testing.strings import random_name
from univention.testing.utils import wait_for_replication, stop_listener, start_listener
import univention.testing.ucr as ucr_test
import univention.testing.udm as udm_test
import univention.testing.utils as utils
from univention.lib.misc import custom_groupname
import sys
RETRY_COUNT = 20
DELAY = 10


class AutoStartStopListener(object):
	"""
	Stops and starts listener automatically
	"""
	def __init__(self, dry_run):  # type: (bool) -> None
		"""
		The listener is only shut down on __enter__ resp. started on __exit__, if dry_run != True.
		>>> with AutoStartStopListener(True):
		>>>     print('Do something with running listener')
		>>> with AutoStartStopListener(True):
		>>>     print('Do something with stopped listener')
		"""
		self.dry_run = dry_run

	def __enter__(self):
		if not self.dry_run:
			stop_listener()
		return self

	def __exit__(self, exc_type, exc_value, traceback):
		if not self.dry_run:
			start_listener()


class TestCases(object):
	def __init__(self, udm, ucr):  # type: (udm_test.UCSTestUDM, ucr_test.UCSTestConfigRegistry) -> None
		self.udm = udm
		self.ucr = ucr
		self.base_user = 'cn=users,%s' % (ucr.get('ldap/base'),)
		self.base_group = 'cn=groups,%s' % (ucr.get('ldap/base'),)
		self.dn_domain_users = 'cn=%s,%s' % (custom_groupname('Domain Users', ucr), self.base_group,)

	def print_attributes(self, dn_list, msg=None):  # type: (List[str], Optional[str]) -> None
		"""
		Prints the DN and the values of the attributes memberOf and uniqueMember for all given
		DNs in dn_list. If msg is specified, a small header line is printed.
		"""
		ATTR_LIST = ['memberOf', 'uniqueMember']
		print()
		if msg is not None:
			print('*** %s ***' % (msg,))
		for dn in dn_list:
			print(dn)
			attrs = self.udm._lo.get(dn, attr=ATTR_LIST)
			for key in ATTR_LIST:
				for val in attrs.get(key, []):
					print('  %s: %s' % (key, val,))
			print()

	def test_user_then_group(self, with_listener):  # type: (bool) -> None
		"""
		1) create user1
		2) create user2
		3) add user1 to group
		4) add user2 to group
		"""
		with AutoStartStopListener(with_listener):
			dn_grp1 = udm.create_object('groups/group', position=self.base_group, name=random_name(), wait_for_replication=with_listener)
			dn_user1 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			dn_user2 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1], wait_for_replication=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1, dn_user2], wait_for_replication=with_listener)
		wait_for_replication()
		self.print_attributes([dn_user1, dn_user2], 'RESULT')
		utils.verify_ldap_object(dn_grp1, {'uniqueMember': [dn_user1, dn_user2]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user1, {'memberOf': [dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user2, {'memberOf': [dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)

	def test_user_group_mixed(self, with_listener):  # type: (bool) -> None
		"""
		1) create user1
		2) add user1 to group
		3) create user2
		4) add user2 to group
		"""
		with AutoStartStopListener(with_listener):
			dn_grp1 = udm.create_object('groups/group', position=self.base_group, name=random_name(), wait_for_replication=with_listener)
			dn_user1 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1], wait_for_replication=with_listener)
			dn_user2 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1, dn_user2], wait_for_replication=with_listener)
		wait_for_replication()
		self.print_attributes([dn_user1, dn_user2], 'RESULT')
		utils.verify_ldap_object(dn_grp1, {'uniqueMember': [dn_user1, dn_user2]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user1, {'memberOf': [dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user2, {'memberOf': [dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)

	def test_move_user(self, with_listener):  # type: (bool) -> None
		"""
		1) create container
		2) create grp1
		3) create user1
		4) create user2
		5) add user1+user2 to group
		6) move user1 to new container
		"""
		with AutoStartStopListener(with_listener):
			dn_cn = udm.create_object('container/cn', position=self.base_user, name=random_name(), wait_for_replication=with_listener)
			dn_grp1 = udm.create_object('groups/group', position=self.base_group, name=random_name(), wait_for_replication=with_listener)
			dn_user1 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			dn_user2 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1, dn_user2], wait_for_replication=with_listener)
			new_dn_user1 = udm.move_object('users/user', dn=dn_user1, position=dn_cn, wait_for_replication=with_listener)
		wait_for_replication()
		self.print_attributes([new_dn_user1, dn_user2], 'RESULT')
		utils.verify_ldap_object(dn_grp1, {'uniqueMember': [new_dn_user1, dn_user2]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(new_dn_user1, {'memberOf': [dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user2, {'memberOf': [dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)

	def test_move_group(self, with_listener):  # type: (bool) -> None
		"""
		1) create container
		2) create grp1
		3) create user1
		4) create user2
		5) add user1+user2 to group
		6) move grp1 to new container
		"""
		with AutoStartStopListener(with_listener):
			dn_cn = udm.create_object('container/cn', position=self.base_group, name=random_name(), wait_for_replication=with_listener)
			dn_grp1 = udm.create_object('groups/group', position=self.base_group, name=random_name(), wait_for_replication=with_listener)
			dn_user1 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			dn_user2 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1, dn_user2], wait_for_replication=with_listener)
			new_dn_grp1 = udm.move_object('groups/group', dn=dn_grp1, position=dn_cn, wait_for_replication=with_listener)
		wait_for_replication()
		self.print_attributes([dn_user1, dn_user2], 'RESULT')
		utils.verify_ldap_object(new_dn_grp1, {'uniqueMember': [dn_user1, dn_user2]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user1, {'memberOf': [new_dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user2, {'memberOf': [new_dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)

	def test_rename_group(self, with_listener):  # type: (bool) -> None
		"""
		1) create grp1
		2) create user1
		3) create user2
		4) add user1+user2 to group
		5) rename grp1
		"""
		with AutoStartStopListener(with_listener):
			dn_grp1 = udm.create_object('groups/group', position=self.base_group, name=random_name(), wait_for_replication=with_listener, wait_for=with_listener)
			dn_user1 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			dn_user2 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1, dn_user2], wait_for_replication=with_listener, wait_for=with_listener)
			new_dn_grp1 = udm.modify_object('groups/group', dn=dn_grp1, name=random_name(), wait_for_replication=with_listener, wait_for=with_listener)
		wait_for_replication()
		self.print_attributes([dn_user1, dn_user2], 'RESULT')
		utils.verify_ldap_object(new_dn_grp1, {'uniqueMember': [dn_user1, dn_user2]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user1, {'memberOf': [new_dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user2, {'memberOf': [new_dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)

	def test_remove_group(self, with_listener):  # type: (bool) -> None
		"""
		1) create grp1
		2) create user1
		3) add user1 to group
		4) wait for replication
		5) create user2
		6) add user2 to group
		7) remove grp1
		"""
		with AutoStartStopListener(with_listener):
			dn_grp1 = udm.create_object('groups/group', position=self.base_group, name=random_name(), wait_for_replication=with_listener, wait_for=with_listener)
			dn_user1 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener, wait_for=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1], wait_for_replication=with_listener, wait_for=with_listener)
		wait_for_replication()
		with AutoStartStopListener(with_listener):
			dn_user2 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener, wait_for=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1, dn_user2], wait_for_replication=with_listener, wait_for=with_listener)
			udm.remove_object('groups/group', dn=dn_grp1, wait_for_replication=with_listener)
		wait_for_replication()
		udm.wait_for('groups/group', dn_grp1, wait_for_s4connector=with_listener)
		self.print_attributes([dn_user1, dn_user2], 'RESULT')
		utils.verify_ldap_object(dn_user1, {'memberOf': [self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user2, {'memberOf': [self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)

	def test_remove_user_from_group(self, with_listener):  # type: (bool) -> None
		"""
		1) create grp1
		2) create user1
		3) add user1 to group
		4) wait for replication
		5) create user2
		6) add user2 to group
		7) remove user1 from group
		"""
		with AutoStartStopListener(with_listener):
			dn_grp1 = udm.create_object('groups/group', position=self.base_group, name=random_name(), wait_for_replication=with_listener, wait_for=with_listener)
			dn_user1 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener, wait_for=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1], wait_for_replication=with_listener, wait_for=with_listener)
		wait_for_replication()
		with AutoStartStopListener(with_listener):
			dn_user2 = udm.create_object('users/user', position=self.base_user, username=random_name(), lastname=random_name(), password=random_name(), wait_for_replication=with_listener, wait_for=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, users=[dn_user1, dn_user2], wait_for_replication=with_listener, wait_for=with_listener)
			udm.modify_object('groups/group', dn=dn_grp1, remove={'users': [dn_user1]}, wait_for_replication=with_listener, wait_for=with_listener)
		wait_for_replication()
		self.print_attributes([dn_user1, dn_user2], 'RESULT')
		utils.verify_ldap_object(dn_grp1, {'uniqueMember': [dn_user2]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user1, {'memberOf': [self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)
		utils.verify_ldap_object(dn_user2, {'memberOf': [dn_grp1, self.dn_domain_users]}, strict=True, retry_count=RETRY_COUNT, delay=DELAY)

	def run(self):
		for func in (
			self.test_user_then_group,
			self.test_user_group_mixed,
			self.test_move_user,
			self.test_move_group,
			self.test_rename_group,
			self.test_remove_group,
			self.test_remove_user_from_group,
		):
			for with_listener in [True, False]:
				print()
				print('*' * (len(func.__name__) + 14))
				print('*** %s(%r)' % (func.__name__, with_listener))
				print('*' * (len(func.__name__) + 14))
				func(with_listener)


if __name__ == '__main__':
	with ucr_test.UCSTestConfigRegistry() as ucr, udm_test.UCSTestUDM() as udm:
		if utils.package_installed('univention-samba4') and ucr.is_false('connector/s4/autostart', False):
			# skip test which would be flaky otherwise
			print('WARNING: univention-samba4 is installed but no running S4 connector on this machine --> skipping test')
			sys.exit(138)

		testcases = TestCases(udm, ucr)
		testcases.run()

# vim: set ft=python :
