<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * LDAP enrolment plugin tests.
 *
 * NOTE: in order to execute this test you need to set up
 *       OpenLDAP server with core, cosine, nis and internet schemas
 *       and add configuration constants to config.php or phpunit.xml configuration file:
 *
 * define('TEST_ENROL_LDAP_HOST_URL', 'ldap://127.0.0.1');
 * define('TEST_ENROL_LDAP_BIND_DN', 'cn=someuser,dc=example,dc=local');
 * define('TEST_ENROL_LDAP_BIND_PW', 'somepassword');
 * define('TEST_ENROL_LDAP_DOMAIN', 'dc=example,dc=local');
 *
 * @package    enrol_ldap
 * @category   phpunit
 * @copyright  2013 Petr Skoda {@link http://skodak.org}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

defined('MOODLE_INTERNAL') || die();

global $CFG;


class enrol_ldap_testcase extends advanced_testcase {

    public function test_enrol_ldap() {
        global $CFG, $DB;

        if (!extension_loaded('ldap')) {
            $this->markTestSkipped('LDAP extension is not loaded.');
        }

        $this->resetAfterTest();

        require_once($CFG->dirroot.'/enrol/ldap/lib.php');
        require_once($CFG->libdir.'/ldaplib.php');

        if (!defined('TEST_ENROL_LDAP_HOST_URL') or !defined('TEST_ENROL_LDAP_BIND_DN') or !defined('TEST_ENROL_LDAP_BIND_PW') or !defined('TEST_ENROL_LDAP_DOMAIN')) {
            $this->markTestSkipped('External LDAP test server not configured.');
        }

        // Make sure we can connect the server.
        $debuginfo = '';
        if (!$connection = ldap_connect_moodle(TEST_ENROL_LDAP_HOST_URL, 3, 'rfc2307', TEST_ENROL_LDAP_BIND_DN, TEST_ENROL_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) {
            $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo);
        }

        $this->enable_plugin();

        // Create new empty test container.
        $topdn = 'dc=moodletest,'.TEST_ENROL_LDAP_DOMAIN;

        $this->recursive_delete($connection, TEST_ENROL_LDAP_DOMAIN, 'dc=moodletest');

        $o = array();
        $o['objectClass'] = array('dcObject', 'organizationalUnit');
        $o['dc']         = 'moodletest';
        $o['ou']         = 'MOODLETEST';
        if (!ldap_add($connection, 'dc=moodletest,'.TEST_ENROL_LDAP_DOMAIN, $o)) {
            $this->markTestSkipped('Can not create test LDAP container.');
        }

        // Configure enrol plugin.
        /** @var enrol_ldap_plugin $enrol */
        $enrol = enrol_get_plugin('ldap');
        $enrol->set_config('host_url', TEST_ENROL_LDAP_HOST_URL);
        $enrol->set_config('start_tls', 0);
        $enrol->set_config('ldap_version', 3);
        $enrol->set_config('ldapencoding', 'utf-8');
        $enrol->set_config('pagesize', '2');
        $enrol->set_config('bind_dn', TEST_ENROL_LDAP_BIND_DN);
        $enrol->set_config('bind_pw', TEST_ENROL_LDAP_BIND_PW);
        $enrol->set_config('course_search_sub', 0);
        $enrol->set_config('memberattribute_isdn', 0);
        $enrol->set_config('user_contexts', '');
        $enrol->set_config('user_search_sub', 0);
        $enrol->set_config('user_type', 'rfc2307');
        $enrol->set_config('opt_deref', LDAP_DEREF_NEVER);
        $enrol->set_config('objectclass', '(objectClass=posixGroup)');
        $enrol->set_config('course_idnumber', 'cn');
        $enrol->set_config('course_shortname', 'cn');
        $enrol->set_config('course_fullname', 'cn');
        $enrol->set_config('course_summary', '');
        $enrol->set_config('ignorehiddencourses', 0);
        $enrol->set_config('nested_groups', 0);
        $enrol->set_config('autocreate', 0);
        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_KEEP);

        $roles = get_all_roles();
        foreach ($roles as $role) {
            $enrol->set_config('contexts_role'.$role->id, '');
            $enrol->set_config('memberattribute_role'.$role->id, '');
        }

        // Create group for teacher enrolments.
        $teacherrole = $DB->get_record('role', array('shortname'=>'teacher'));
        $this->assertNotEmpty($teacherrole);
        $o = array();
        $o['objectClass'] = array('organizationalUnit');
        $o['ou']          = 'teachers';
        ldap_add($connection, 'ou=teachers,'.$topdn, $o);
        $enrol->set_config('contexts_role'.$teacherrole->id, 'ou=teachers,'.$topdn);
        $enrol->set_config('memberattribute_role'.$teacherrole->id, 'memberuid');

        // Create group for student enrolments.
        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
        $this->assertNotEmpty($studentrole);
        $o = array();
        $o['objectClass'] = array('organizationalUnit');
        $o['ou']          = 'students';
        ldap_add($connection, 'ou=students,'.$topdn, $o);
        $enrol->set_config('contexts_role'.$studentrole->id, 'ou=students,'.$topdn);
        $enrol->set_config('memberattribute_role'.$studentrole->id, 'memberuid');

        // Create some users and courses.
        $user1 = $this->getDataGenerator()->create_user(array('idnumber'=>'user1', 'username'=>'user1'));
        $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'user2', 'username'=>'user2'));
        $user3 = $this->getDataGenerator()->create_user(array('idnumber'=>'user3', 'username'=>'user3'));
        $user4 = $this->getDataGenerator()->create_user(array('idnumber'=>'user4', 'username'=>'user4'));
        $user5 = $this->getDataGenerator()->create_user(array('idnumber'=>'user5', 'username'=>'user5'));
        $user6 = $this->getDataGenerator()->create_user(array('idnumber'=>'user6', 'username'=>'user6'));

        $course1 = $this->getDataGenerator()->create_course(array('idnumber'=>'course1', 'shortname'=>'course1'));
        $course2 = $this->getDataGenerator()->create_course(array('idnumber'=>'course2', 'shortname'=>'course2'));
        $course3 = $this->getDataGenerator()->create_course(array('idnumber'=>'course3', 'shortname'=>'course3'));

        // Set up some ldap data.
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        $o['memberUid']   = array('user1', 'user2', 'user3', 'userx');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '2';
        $o['memberUid']   = array('user5');
        ldap_add($connection, 'cn='.$o['cn'].',ou=teachers,'.$topdn, $o);

        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course2';
        $o['gidNumber']   = '3';
        $o['memberUid']   = array('user1', 'user2', 'user3', 'user4');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course4';
        $o['gidNumber']   = '4';
        $o['memberUid']   = array('user1', 'user2');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course4';
        $o['gidNumber']   = '5';
        $o['memberUid']   = array('user5', 'user6');
        ldap_add($connection, 'cn='.$o['cn'].',ou=teachers,'.$topdn, $o);


        // Test simple test without creation.

        $this->assertEquals(0, $DB->count_records('user_enrolments'));
        $this->assertEquals(0, $DB->count_records('role_assignments'));
        $this->assertEquals(4, $DB->count_records('course'));

        $enrol->sync_enrolments(new null_progress_trace());

        $this->assertEquals(8, $DB->count_records('user_enrolments'));
        $this->assertEquals(8, $DB->count_records('role_assignments'));
        $this->assertEquals(4, $DB->count_records('course'));

        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id);
        $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id);
        $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id);
        $this->assertIsEnrolled($course1->id, $user5->id, $teacherrole->id);

        $this->assertIsEnrolled($course2->id, $user1->id, $studentrole->id);
        $this->assertIsEnrolled($course2->id, $user2->id, $studentrole->id);
        $this->assertIsEnrolled($course2->id, $user3->id, $studentrole->id);
        $this->assertIsEnrolled($course2->id, $user4->id, $studentrole->id);


        // Test course creation.
        $enrol->set_config('autocreate', 1);

        $enrol->sync_enrolments(new null_progress_trace());

        $this->assertEquals(12, $DB->count_records('user_enrolments'));
        $this->assertEquals(12, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));

        $course4 = $DB->get_record('course', array('idnumber'=>'course4'), '*', MUST_EXIST);

        $this->assertIsEnrolled($course4->id, $user1->id, $studentrole->id);
        $this->assertIsEnrolled($course4->id, $user2->id, $studentrole->id);
        $this->assertIsEnrolled($course4->id, $user5->id, $teacherrole->id);
        $this->assertIsEnrolled($course4->id, $user6->id, $teacherrole->id);


        // Test unenrolment.
        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_KEEP);
        $enrol->sync_enrolments(new null_progress_trace());
        $this->assertEquals(12, $DB->count_records('user_enrolments'));
        $this->assertEquals(12, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));

        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND);
        $enrol->sync_enrolments(new null_progress_trace());
        $this->assertEquals(12, $DB->count_records('user_enrolments'));
        $this->assertEquals(12, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_SUSPENDED);
        $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id, ENROL_USER_SUSPENDED);
        $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id, ENROL_USER_SUSPENDED);

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        $o['memberUid']   = array('user1', 'user2', 'user3');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->sync_enrolments(new null_progress_trace());
        $this->assertEquals(12, $DB->count_records('user_enrolments'));
        $this->assertEquals(12, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);
        $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id, ENROL_USER_ACTIVE);
        $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id, ENROL_USER_ACTIVE);

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
        $enrol->sync_enrolments(new null_progress_trace());
        $this->assertEquals(12, $DB->count_records('user_enrolments'));
        $this->assertEquals(9, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, 0, ENROL_USER_SUSPENDED);
        $this->assertIsEnrolled($course1->id, $user2->id, 0, ENROL_USER_SUSPENDED);
        $this->assertIsEnrolled($course1->id, $user3->id, 0, ENROL_USER_SUSPENDED);

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        $o['memberUid']   = array('user1', 'user2', 'user3');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->sync_enrolments(new null_progress_trace());
        $this->assertEquals(12, $DB->count_records('user_enrolments'));
        $this->assertEquals(12, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);
        $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id, ENROL_USER_ACTIVE);
        $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id, ENROL_USER_ACTIVE);

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL);
        $enrol->sync_enrolments(new null_progress_trace());
        $this->assertEquals(9, $DB->count_records('user_enrolments'));
        $this->assertEquals(9, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsNotEnrolled($course1->id, $user1->id);
        $this->assertIsNotEnrolled($course1->id, $user2->id);
        $this->assertIsNotEnrolled($course1->id, $user3->id);


        // Individual user enrolments-

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        $o['memberUid']   = array('user1', 'user2', 'user3');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->sync_user_enrolments($user1);
        $this->assertEquals(10, $DB->count_records('user_enrolments'));
        $this->assertEquals(10, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        $o['memberUid']   = array('user2', 'user3');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_KEEP);
        $enrol->sync_user_enrolments($user1);
        $this->assertEquals(10, $DB->count_records('user_enrolments'));
        $this->assertEquals(10, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);

        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND);
        $enrol->sync_user_enrolments($user1);
        $this->assertEquals(10, $DB->count_records('user_enrolments'));
        $this->assertEquals(10, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_SUSPENDED);

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        $o['memberUid']   = array('user1', 'user2', 'user3');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->sync_user_enrolments($user1);
        $this->assertEquals(10, $DB->count_records('user_enrolments'));
        $this->assertEquals(10, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        $o['memberUid']   = array('user2', 'user3');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
        $enrol->sync_user_enrolments($user1);
        $this->assertEquals(10, $DB->count_records('user_enrolments'));
        $this->assertEquals(9, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, 0, ENROL_USER_SUSPENDED);

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        $o['memberUid']   = array('user1', 'user2', 'user3');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->sync_user_enrolments($user1);
        $this->assertEquals(10, $DB->count_records('user_enrolments'));
        $this->assertEquals(10, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);

        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
        $o = array();
        $o['objectClass'] = array('posixGroup');
        $o['cn']          = 'course1';
        $o['gidNumber']   = '1';
        $o['memberUid']   = array('user2', 'user3');
        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);

        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL);
        $enrol->sync_user_enrolments($user1);
        $this->assertEquals(9, $DB->count_records('user_enrolments'));
        $this->assertEquals(9, $DB->count_records('role_assignments'));
        $this->assertEquals(5, $DB->count_records('course'));
        $this->assertIsNotEnrolled($course1->id, $user1->id);

        $this->recursive_delete($connection, TEST_ENROL_LDAP_DOMAIN, 'dc=moodletest');
        ldap_close($connection);

        // NOTE: multiple roles in one course is not supported, sorry
    }

    public function assertIsEnrolled($courseid, $userid, $roleid, $status=null) {
        global $DB;

        $context = context_course::instance($courseid);
        $instance = $DB->get_record('enrol', array('courseid'=>$courseid, 'enrol'=>'ldap'));
        $this->assertNotEmpty($instance);
        $ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid));
        $this->assertNotEmpty($ue);
        if (isset($status)) {
            $this->assertEquals($status, $ue->status);
        }
        if ($roleid) {
            $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context->id, 'userid'=>$userid, 'roleid'=>$roleid, 'component'=>'enrol_ldap')));
        } else {
            $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context->id, 'userid'=>$userid, 'component'=>'enrol_ldap')));
        }
    }

    public function assertIsNotEnrolled($courseid, $userid) {
        $context = context_course::instance($courseid);
        $this->assertFalse(is_enrolled($context, $userid));
    }

    protected function enable_plugin() {
        $enabled = enrol_get_plugins(true);
        $enabled['ldap'] = true;
        $enabled = array_keys($enabled);
        set_config('enrol_plugins_enabled', implode(',', $enabled));
    }

    protected function disable_plugin() {
        $enabled = enrol_get_plugins(true);
        unset($enabled['ldap']);
        $enabled = array_keys($enabled);
        set_config('enrol_plugins_enabled', implode(',', $enabled));
    }

    protected function recursive_delete($connection, $dn, $filter) {
        if ($res = ldap_list($connection, $dn, $filter, array('dn'))) {
            $info = ldap_get_entries($connection, $res);
            ldap_free_result($res);
            if ($info['count'] > 0) {
                if ($res = ldap_search($connection, "$filter,$dn", 'cn=*', array('dn'))) {
                    $info = ldap_get_entries($connection, $res);
                    ldap_free_result($res);
                    foreach ($info as $i) {
                        if (isset($i['dn'])) {
                            ldap_delete($connection, $i['dn']);
                        }
                    }
                }
                if ($res = ldap_search($connection, "$filter,$dn", 'ou=*', array('dn'))) {
                    $info = ldap_get_entries($connection, $res);
                    ldap_free_result($res);
                    foreach ($info as $i) {
                        if (isset($i['dn']) and $info[0]['dn'] != $i['dn']) {
                            ldap_delete($connection, $i['dn']);
                        }
                    }
                }
                ldap_delete($connection, "$filter,$dn");
            }
        }
    }
}
