diff --git a/.gitignore b/.gitignore index 7a35f1c52920b69500bfaab746340feca0b8c616..356e960ab077264135223c60c1347db1f71ea818 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ mahara-*.zip /test/.project /test/.buildpath /test/.settings +/test/behat/selenium-server-standalone* /configure-stamp /debian/files /debian/mahara-apache.postinst.debhelper @@ -25,3 +26,4 @@ mahara-*.zip /debian/mahara-apache2.substvars /debian/mahara-apache2 .DS_Store +/external diff --git a/htdocs/composer.json b/external/composer.json similarity index 61% rename from htdocs/composer.json rename to external/composer.json index 033411299c133f0c7d8f54c28684d4eecd416c22..fdddbec5a0e9752a06fa147190e94a024d137580 100644 --- a/htdocs/composer.json +++ b/external/composer.json @@ -1,14 +1,15 @@ { "require": { "php": ">=5.3.2", - "behat/behat": "2.5.1", - "behat/mink": "1.5.0", + "behat/behat": "~2.5", + "behat/mink": "*", "behat/mink-extension": "*", "behat/mink-goutte-driver": "*", "behat/mink-selenium-driver": "*", - "behat/mink-selenium2-driver": "*" + "behat/mink-selenium2-driver": "*", + "fabpot/goutte": "~1.0" }, "minimum-stability": "dev" diff --git a/htdocs/account/tests/behat/BehatAccount.php b/htdocs/account/tests/behat/BehatAccount.php deleted file mode 100644 index 1d159755c3a83679d59d29bc9aa1ba0f600152ae..0000000000000000000000000000000000000000 --- a/htdocs/account/tests/behat/BehatAccount.php +++ /dev/null @@ -1,32 +0,0 @@ -This is my new journal
| - | tags | blog | - And I press "Create journal" - Then I should see "My new journal" \ No newline at end of file diff --git a/htdocs/lib/db/upgrade.php b/htdocs/lib/db/upgrade.php index 9ac0163f984f27558ce57f22b2ced98ebed9bfd5..8dfb5a278175dae92b9c7f60bf9b9b15d797001c 100644 --- a/htdocs/lib/db/upgrade.php +++ b/htdocs/lib/db/upgrade.php @@ -338,7 +338,7 @@ function xmldb_core_upgrade($oldversion=0) { delete_records('usr_watchlist_view','view',$viewid); if ($blockinstanceids = get_column('block_instance', 'id', 'view', $viewid)) { foreach ($blockinstanceids as $id) { - if (table_exists('blocktype_wall_post')) { + if (table_exists(new XMLDBTable('blocktype_wall_post'))) { delete_records('blocktype_wall_post', 'instance', $id); } delete_records('view_artefact', 'block', $id); diff --git a/htdocs/lib/ddl.php b/htdocs/lib/ddl.php index 469061d43a39af4d5a1d1140e2d0afac283a2a1e..49266c36c02146cb0d65e1a8fa873f37ed67cc32 100644 --- a/htdocs/lib/ddl.php +++ b/htdocs/lib/ddl.php @@ -682,7 +682,9 @@ function uninstall_from_xmldb_file($file) { if ($tables = array_reverse($structure->getTables())) { foreach ($tables as $table) { - if ($indexes = $table->getIndexes()) { + // for MySQL, skip dropping indexs and keys + // as they will be dropped when the table is dropped + if (!is_mysql() && $indexes = $table->getIndexes()) { foreach ($indexes as $index) { if ($index->getName() == 'usernameuk' && is_postgres()) { // this is a giant hack, but adodb cannot handle resolving @@ -694,7 +696,7 @@ function uninstall_from_xmldb_file($file) { drop_index($table, $index); } } - if ($keys = $table->getKeys()) { + if (!is_mysql() && $keys = $table->getKeys()) { $sortkeys = array(); foreach ($keys as $key) { $sortkeys[] = $key->type; @@ -1413,63 +1415,189 @@ function rename_index($table, $index, $newname, $continue=true, $feedback=true) } /** - * Return all tables in current db + * Return structure info of tables from a xmldb file * - * @return array('tablename' => 'tablename', ...) - * Note all table names is in lower cases + * @param string $file + * @return array(XMLDBTable) + * @throws InstallationException */ -function get_tables() { - +function get_tables_from_xmldb_file($file) { global $CFG, $db; - // Get all tables in current DB - $tables = $metatables = $db->MetaTables('TABLES'); - if (!empty($CFG->prefix)) { - $tables = array(); - foreach ($metatables as $mtable) { - if (strpos($mtable, $CFG->prefix) !== false) { - $tables[] = $mtable; + $status = true; + $xmldb_file = new XMLDBFile($file); + + if (!$xmldb_file->fileExists()) { + throw new InstallationException($xmldb_file->path . " doesn't exist."); + } + + $loaded = $xmldb_file->loadXMLStructure(); + if (!$loaded || !$xmldb_file->isLoaded()) { + throw new InstallationException("Could not load " . $xmldb_file->path); + } + + $structure = $xmldb_file->getStructure(); + + return array_reverse($structure->getTables()); + +} + +/** + * Return structure info of tables from mahara xmldb files + * + * @return array(XMLDBTable) + */ +function get_tables_from_xmldb() { + static $tables = array(); + if (!empty($tables)) { + return $tables; + } + // Get database structure from plugins' tables + foreach (array_reverse(plugin_types_installed()) as $t) { + if ($installed = plugins_installed($t, true)) { + foreach ($installed as $p) { + $location = get_config('docroot') . $t . '/' . $p->name. '/db/'; + if (is_readable($location . 'install.xml')) { + $tables = array_merge($tables, get_tables_from_xmldb_file($location . 'install.xml')); + } } } } - unset($metatables); - $tnames = array(); - foreach ($tables as $t) { - $t = strtolower($t); - $tnames[$t] = $t; - } + $tables = array_merge($tables, get_tables_from_xmldb_file(get_config('docroot') . 'lib/db/install.xml')); - return $tnames; + return $tables; } /** * Return all columns of a table in current db * - * @param string $tablename should be a full name including the dbprefix + * @param string $tablename not including the dbprefix * @return array of ADOFieldObject */ function get_columns($tablename) { global $CFG, $db; - $columns = $db->MetaColumns($tablename); - // Update the field Auto_increment if postgres - // Only apply for "id" field - if (is_postgres()) { - if (isset($columns['id'])) { - $idcolumn = $columns['id']; - if (isset($idcolumn->primary_key) && ($idcolumn->primary_key === 1) - && isset($idcolumn->default_value) - && strpos($idcolumn->default_value, 'nextval(') !== false ) { - $rec = get_record_sql('SELECT last_value FROM '. "{$tablename}" . '_id_seq'); - $idcolumn->Auto_increment = $rec->last_value + 1; + $fulltablename = $CFG->dbprefix . $tablename; + $columns = $db->MetaColumns($fulltablename); + // Update the field auto_increment if postgres + // Only apply for "ID" field + if (is_postgres() && isset($columns['ID'])) { + $idcolumn = $columns['ID']; + if (isset($idcolumn->default_value) + && strpos($idcolumn->default_value, 'nextval(') !== false ) { + if (record_exists($tablename)) { + $rec = get_record_sql('SELECT last_value FROM "' . $fulltablename . '_id_seq"'); + $idcolumn->auto_increment = $rec->last_value + 1; + } + else { + $idcolumn->auto_increment = 1; } - $columns['id'] = $idcolumn; } + $columns['ID'] = $idcolumn; } return $columns; } +/** + * Return current foreign key constraints in given table + * + * @param string $tablename not including the dbprefix + * @return array of array( + * 'constraintname' => string + * 'table' => string + * 'fields' => array + * 'reftable' => string + * 'reffields' => array + * ) + */ +function get_foreign_keys($tablename) { + global $CFG; + + $tablename = $CFG->dbprefix . $tablename; + $foreignkeys = array(); + // Get foreign key constraints from information_schema tables + if (is_postgres()) { + $dbfield = 'catalog'; + // The query to find all the columns for a foreign key constraint + $fkcolsql = " + SELECT + ku.column_name, + ccu.table_name AS reftable_name, + ccu.column_name AS refcolumn_name + FROM + information_schema.key_column_usage ku + INNER JOIN information_schema.constraint_column_usage ccu + ON ku.constraint_name = ccu.constraint_name + AND ccu.constraint_schema = ku.constraint_schema + AND ccu.constraint_catalog = ku.constraint_catalog + AND ccu.table_catalog = ku.constraint_catalog + AND ccu.table_schema = ku.constraint_schema + WHERE + ku.constraint_catalog = ? + AND ku.constraint_name = ? + AND ku.table_name = ? + AND ku.table_catalog = ? + ORDER BY ku.ordinal_position, ku.position_in_unique_constraint + "; + } + else { + $dbfield = 'schema'; + // The query to find all the columns for a foreign key constraint + $fkcolsql = ' + SELECT + ku.column_name, + ku.referenced_table_name AS reftable_name, + ku.referenced_column_name AS refcolumn_name + FROM information_schema.key_column_usage ku + WHERE + ku.constraint_schema = ? + AND ku.constraint_name = ? + AND ku.table_name = ? + AND ku.table_schema = ? + ORDER BY ku.ordinal_position, ku.position_in_unique_constraint + '; + } + $sql = " + SELECT tc.constraint_name + FROM information_schema.table_constraints tc + WHERE + tc.table_name = ? + AND tc.table_{$dbfield} = ? + AND tc.constraint_{$dbfield} = ? + AND tc.constraint_type = ? + "; + $dbname = get_config('dbname'); + if ($constraintrec = get_records_sql_array($sql, array($tablename, $dbname, $dbname, 'FOREIGN KEY'))) { + // Get foreign key constraint info + foreach ($constraintrec as $c) { + $fields = array(); + $reftable = ''; + $reffields = array(); + if ($colrecs = get_records_sql_array($fkcolsql, array($dbname, $c->constraint_name, $tablename, $dbname))) { + foreach ($colrecs as $colrec) { + if (empty($reftable)) { + $reftable = $colrec->reftable_name; + } + $fields[] = $colrec->column_name; + $reffields[] = $colrec->refcolumn_name; + } + } + if (!empty($fields) && !empty($reftable) && !empty($reffields)) { + $foreignkeys[] = array( + 'table' => $tablename, + 'constraintname' => $c->constraint_name, + 'fields' => $fields, + 'reftable' => $reftable, + 'reffields' => $reffields, + ); + } + } + } + + return $foreignkeys; +} + /** * Return the server info * diff --git a/htdocs/lib/view.php b/htdocs/lib/view.php index 4660e3fc003d97764f2be3c8d281150e90ab1b8b..cb323f003b14eb7b3d4fc04e48feda284cd0e465 100644 --- a/htdocs/lib/view.php +++ b/htdocs/lib/view.php @@ -612,7 +612,7 @@ class View { return new View($view->get('id')); // Reread to ensure defaults are set } - public function default_columnsperrow() { + public static function default_columnsperrow() { $default = array(1 => (object)array('row' => 1, 'columns' => 3, 'widths' => '33,33,33')); if (!$id = get_field('view_layout_columns', 'id', 'columns', $default[1]->columns, 'widths', $default[1]->widths)) { throw new SystemException("View::default_columnsperrow: Default columns = 3, widths = '33,33,33' not in view_layout_columns table"); diff --git a/htdocs/lib/xmldb/classes/XMLDBObject.class.php b/htdocs/lib/xmldb/classes/XMLDBObject.class.php index 3471d4b8f29fadcbc676eb6773b9a22dd526cc12..cf845adfe4073dbd6ba6c0fcb5daa9a049b96eec 100644 --- a/htdocs/lib/xmldb/classes/XMLDBObject.class.php +++ b/htdocs/lib/xmldb/classes/XMLDBObject.class.php @@ -164,7 +164,7 @@ class XMLDBObject { function checkName () { $result = true; - if ($this->name != eregi_replace('[^a-z0-9_ -]', '', $this->name)) { + if ($this->name != preg_replace('/[^a-z0-9_ -]/i', '', $this->name)) { $result = false; } return $result; diff --git a/htdocs/testing/classes/generator/data_generator_base.php b/htdocs/testing/classes/generator/DataGeneratorBase.php similarity index 100% rename from htdocs/testing/classes/generator/data_generator_base.php rename to htdocs/testing/classes/generator/DataGeneratorBase.php diff --git a/htdocs/testing/classes/generator/TestingDataGenerator.php b/htdocs/testing/classes/generator/TestingDataGenerator.php new file mode 100644 index 0000000000000000000000000000000000000000..a0b72deab530fbdc97d9b7aa327abad7785ed0e4 --- /dev/null +++ b/htdocs/testing/classes/generator/TestingDataGenerator.php @@ -0,0 +1,402 @@ +usercounter = 0; + $this->$groupcount = 0; + $this->$institutioncount = 0; + + foreach ($this->generators as $generator) { + $generator->reset(); + } + } + + /** + * Return generator for given plugin. + * @param string $plugintype the plugin type, e.g. 'artefact' or 'blocktype'. + * @param string $pluginname the plugin name, e.g. 'blog' or 'file'. + * @return an instance of a plugin generator extending from CoreGenerator. + */ + public function get_plugin_generator($plugintype, $pluginname) { + $pluginfullname = "{$plugintype}.{$pluginname}"; + if (isset($this->generators[$pluginfullname])) { + return $this->generators[$pluginfullname]; + } + safe_require($plugintype, $pluginname, 'tests/generator/lib.php'); + + $classname = generate_generator_class_name($plugintype, $pluginname); + + if (!class_exists($classname)) { + throw new UndefinedException("The plugin $pluginfullname does not support " . + "data generators yet. Class {$classname} not found."); + } + + $this->generators[$pluginfullname] = new $classname($this); + return $this->generators[$pluginfullname]; + + } + + /** + * Create a test user + * @param array $record + * @throws SystemException if creating failed + * @return int new user id + */ + public function create_user($record) { + // Data validation + // Set default auth method for a new user is 'internal' for 'No institution' if not set + if (empty($record['institution']) || empty($record['authname'])) { + $record['institution'] = 'mahara'; + $record['authname'] = 'internal'; + } + if (!$auth = get_record('auth_instance', 'institution', $record['institution'], 'authname', $record['authname'])) { + throw new SystemException("The authentication method authname" . $record['authname'] . " for institution '" . $record['institution'] . "' does not exist."); + } + $record['authinstance'] = $auth->id; + // Don't exceed max user accounts for the institution + $institution = new Institution($record['institution']); + if ($institution->isFull()) { + throw new SystemException("Can not add new users to the institution '" . $record['institution'] . "' as it is full."); + } + + $record['firstname'] = sanitize_firstname($record['firstname']); + $record['lastname'] = sanitize_lastname($record['lastname']); + $record['email'] = sanitize_email($record['email']); + + $authobj = AuthFactory::create($auth->id); + if (method_exists($authobj, 'is_username_valid_admin') && !$authobj->is_username_valid_admin($record['username'])) { + throw new SystemException("New username'" . $record['username'] . "' is not valid."); + } + if (method_exists($authobj, 'is_username_valid') && !$authobj->is_username_valid($record['username'])) { + throw new SystemException("New username'" . $record['username'] . "' is not valid."); + } + if (record_exists_select('usr', 'LOWER(username) = ?', strtolower($record['username']))) { + throw new ErrorException("The username'" . $record['username'] . "' has been taken."); + } + if (method_exists($authobj, 'is_password_valid') && !$authobj->is_password_valid($record['password'])) { + throw new ErrorException("The password'" . $record['password'] . "' is not valid."); + } + if (record_exists('usr', 'email', $record['email']) + || record_exists('artefact_internal_profile_email', 'email', $record['email'])) { + throw new ErrorException("The email'" . $record['email'] . "' has been taken."); + } + + // Create new user + db_begin(); + raise_time_limit(180); + + $user = (object)array( + 'authinstance' => $record['authinstance'], + 'username' => $record['username'], + 'firstname' => $record['firstname'], + 'lastname' => $record['lastname'], + 'email' => $record['email'], + 'password' => $record['password'], + 'passwordchange' => 0, + ); + if ($record['institution'] == 'mahara') { + if ($record['role'] == 'admin') { + $user->admin = 1; + } + else if ($record['role'] == 'staff') { + $user->staff = 1; + } + } + + $remoteauth = $record['authname'] != 'internal'; + if (!isset($record['remoteusername'])) { + $record['remoteusername'] = null; + } + + $user->id = create_user($user, array(), $record['institution'], $remoteauth, $record['remoteusername'], $record); + + if (isset($user->admin) && $user->admin) { + require_once('activity.php'); + activity_add_admin_defaults(array($user->id)); + } + + if ($record['institution'] != 'mahara') { + if ($record['role'] == 'admin') { + set_field('usr_institution', 'admin', 1, 'usr', $user->id, 'institution', $record['institution']); + } + else if ($record['role'] == 'staff') { + set_field('usr_institution', 'staff', 1, 'usr', $user->id, 'institution', $record['institution']); + } + } + + db_commit(); + $this->usercounter++; + return $user->id; + } + + /** + * Create a test group + * @param array $record + * @throws ErrorException if creating failed + * @return int new group id + */ + public function create_group($record) { + // Data validation + $record['name'] = trim($record['name']); + if ($ids = get_records_sql_array('SELECT id FROM {group} WHERE LOWER(TRIM(name)) = ?', array(strtolower($record['name'])))) { + if (count($ids) > 1 || $ids[0]->id != $group_data->id) { + throw new SystemException("Invalid group name '" . $record['name'] . "'. " . get_string('groupalreadyexists', 'group')); + } + } + $record['owner'] = trim($record['owner']); + $ids = get_records_sql_array('SELECT id FROM {usr} WHERE LOWER(TRIM(username)) = ?', array(strtolower($record['owner']))); + if (!$ids || count($ids) > 1) { + throw new SystemException("Invalid group owner '" . $record['owner'] . "'. The username does not exist or duplicated"); + } + $members = array($ids[0]->id => 'admin'); + if (!empty($record['members'])) { + foreach (explode(',', $record['members']) as $membername) { + $ids = get_records_sql_array('SELECT id FROM {usr} WHERE LOWER(TRIM(username)) = ?', array(strtolower(trim($membername)))); + if (!$ids || count($ids) > 1) { + throw new SystemException("Invalid group member '" . $membername . "'. The username does not exist or duplicated"); + } + $members[$ids[0]->id] = 'member'; + } + } + if (!empty($record['staff'])) { + foreach (explode(',', $record['staff']) as $membername) { + $ids = get_records_sql_array('SELECT id FROM {usr} WHERE LOWER(TRIM(username)) = ?', array(strtolower(trim($membername)))); + if (!$ids || count($ids) > 1) { + throw new SystemException("Invalid group staff '" . $membername . "'. The username does not exist or duplicated"); + } + $members[$ids[0]->id] = 'staff'; + } + } + if (!empty($record['admins'])) { + foreach (explode(',', $record['admins']) as $membername) { + $ids = get_records_sql_array('SELECT id FROM {usr} WHERE LOWER(TRIM(username)) = ?', array(strtolower(trim($membername)))); + if (!$ids || count($ids) > 1) { + throw new SystemException("Invalid group admin '" . $membername . "'. The username does not exist or duplicated"); + } + $members[$ids[0]->id] = 'admin'; + } + } + $availablegrouptypes = group_get_grouptypes(); + if (!in_array($record['grouptype'], $availablegrouptypes)) { + throw new SystemException("Invalid grouptype '" . $record['grouptype'] . "'. This grouptype does not exist.\n" + . "The available grouptypes are " . join(', ', $availablegrouptypes)); + } + $availablegroupeditroles = array_keys(group_get_editroles_options()); + if (!in_array($record['editroles'], $availablegroupeditroles)) { + throw new SystemException("Invalid group editroles '" . $record['editroles'] . "'. This edit role does not exist.\n" + . "The available group editroles are " . join(', ', $availablegroupeditroles)); + } + if (!empty($record['open'])) { + if (!empty($record['controlled'])) { + throw new SystemException('Invalid group membership setting. ' . get_string('membershipopencontrolled', 'group')); + } + if (!empty($record['request'])) { + throw new SystemException('Invalid group membership setting. ' . get_string('membershipopenrequest', 'group')); + } + } + if (!empty($record['invitefriends']) && !empty($record['suggestfriends'])) { + throw new SystemException('Invalid friend invitation setting. ' . get_string('suggestinvitefriends', 'group')); + } + if (!empty($record['suggestfriends']) && empty($record['open']) && empty($record['request'])) { + throw new SystemException('Invalid friend invitation setting. ' . get_string('suggestfriendsrequesterror', 'group')); + } + if (!empty($record['editwindowstart']) && !empty($record['editwindowend']) && ($record['editwindowstart'] >= $record['editwindowend'])) { + throw new SystemException('Invalid group editability setting. ' . get_string('editwindowendbeforestart', 'group')); + } + $group_data = array( + 'id' => null, + 'name' => $record['name'], + 'description' => isset($record['description']) ? $record['description'] : null, + 'grouptype' => $record['grouptype'], + 'open' => isset($record['open']) ? $record['open'] : 1, + 'controlled' => isset($record['controlled']) ? $record['controlled'] : 0, + 'request' => isset($record['request']) ? $record['request'] : 0, + 'invitefriends' => isset($record['invitefriends']) ? $record['invitefriends'] : 0, + 'suggestfriends' => isset($record['suggestfriends']) ? $record['suggestfriends'] : 0, + 'category' => null, + 'public' => 0, + 'usersautoadded' => 0, + 'viewnotify' => GROUP_ROLES_ALL, + 'submittableto' => isset($record['submittableto']) ? $record['submittableto'] : 0, + 'allowarchives' => isset($record['allowarchives']) ? $record['allowarchives'] : 0, + 'editroles' => isset($record['editroles']) ? $record['editroles'] : 'all', + 'hidden' => 0, + 'hidemembers' => 0, + 'hidemembersfrommembers' => 0, + 'groupparticipationreports' => 0, + 'urlid' => null, + 'editwindowstart' => isset($record['editwindowstart']) ? $record['editwindowstart'] : null, + 'editwindowend' => isset($record['editwindowend']) ? $record['editwindowend'] : null, + 'sendnow' => 0, + 'feedbacknotify' => GROUP_ROLES_ALL, + 'members' => $members, + ); + + // Create a new group + db_begin(); + $group_data['id'] = group_create($group_data); + db_commit(); + + $this->groupcount++; + return $group_data['id']; + } + + /** + * Create a test institution + * @param array $record + * @throws ErrorException if creating failed + * @return int new institution id + */ + public function create_institution($record) { + // Data validation + if (!empty($record['name']) && record_exists('institution', 'name', $record['name'])) { + throw new SystemException("Invalid institution name '" . $record['name'] . "'. " . get_string('institutionnamealreadytaken', 'admin')); + } + + if (get_config('licensemetadata') && !empty($record['licensemandatory']) && + (isset($record['licensedefault']) && $record['licensedefault'] == '')) { + throw new SystemException("Invalid institution license setting. " . get_string('licensedefaultmandatory', 'admin')); + } + + if (!empty($record['lang']) && $record['lang'] != 'sitedefault' && !array_key_exists($record['lang'], get_languages())) { + throw new SystemException("Invalid institution language setting: '" . $record['lang'] . "'. This language is not installed for the site."); + } + // Create a new institution + db_begin(); + // Update the basic institution record... + $newinstitution = new Institution(); + $newinstitution->initialise($record['name'], $record['displayname']); + $institution = $newinstitution->name; + + $newinstitution->showonlineusers = !isset($record['showonlineusers']) ? 2 : $record['showonlineusers']; + if (get_config('usersuniquebyusername')) { + // Registering absolutely not allowed when this setting is on, it's a + // security risk. See the documentation for the usersuniquebyusername + // setting for more information + $newinstitution->registerallowed = 0; + } + else { + $newinstitution->registerallowed = !empty($record['registerallowed']) ? 1 : 0; + $newinstitution->registerconfirm = !empty($record['registerconfirm']) ? 1 : 0; + } + + if (!empty($record['lang'])) { + if ($record['lang'] == 'sitedefault') { + $newinstitution->lang = null; + } + else { + $newinstitution->lang = $record['lang']; + } + } + + $newinstitution->theme = (empty($record['theme']) || $record['theme'] == 'sitedefault') ? null : $record['theme']; + $newinstitution->dropdownmenu = (!empty($record['dropdownmenu'])) ? 1 : 0; + $newinstitution->skins = (!empty($record['skins'])) ? 1 : 0; + $newinstitution->style = null; + + if (get_config('licensemetadata')) { + $newinstitution->licensemandatory = (!empty($record['licensemandatory'])) ? 1 : 0; + $newinstitution->licensedefault = (isset($record['licensedefault'])) ? $record['licensedefault'] : ''; + } + + $newinstitution->defaultquota = empty($record['defaultquota']) ? get_config_plugin('artefact', 'file', 'defaultquota') : $record['defaultquota']; + + $newinstitution->defaultmembershipperiod = !empty($record['defaultmembershipperiod']) ? intval($record['defaultmembershipperiod']) : null; + $newinstitution->maxuseraccounts = !empty($record['maxuseraccounts']) ? intval($record['maxuseraccounts']) : null; + $newinstitution->expiry = !empty($record['expiry']) ? db_format_timestamp($record['expiry']) : null; + + $newinstitution->allowinstitutionpublicviews = (isset($record['allowinstitutionpublicviews']) && $record['allowinstitutionpublicviews']) ? 1 : 0; + + // Save the changes to the DB + $newinstitution->commit(); + + // Automatically create an internal authentication authinstance + $authinstance = (object)array( + 'instancename' => 'internal', + 'priority' => 0, + 'institution' => $newinstitution->name, + 'authname' => 'internal', + ); + insert_record('auth_instance', $authinstance); + + // We need to add the default lines to the site_content table for this institution + // We also need to set the institution to be using default static pages to begin with + // so that using custom institution pages is an opt-in situation + $pages = site_content_pages(); + $now = db_format_timestamp(time()); + foreach ($pages as $name) { + $page = new stdClass(); + $page->name = $name; + $page->ctime = $now; + $page->mtime = $now; + $page->content = get_string($page->name . 'defaultcontent', 'install', get_string('staticpageconfiginstitution', 'install')); + $page->institution = $newinstitution->name; + insert_record('site_content', $page); + + $institutionconfig = new stdClass(); + $institutionconfig->institution = $newinstitution->name; + $institutionconfig->field = 'sitepages_' . $name; + $institutionconfig->value = 'mahara'; + insert_record('institution_config', $institutionconfig); + } + + db_commit(); + } +} diff --git a/htdocs/testing/classes/generator/lib.php b/htdocs/testing/classes/generator/lib.php index 1021e6ccc2a21bb1446fbcc0f8cabc3769f75058..784cf603420cc77efe52ed87da6adb3daf24d000 100644 --- a/htdocs/testing/classes/generator/lib.php +++ b/htdocs/testing/classes/generator/lib.php @@ -16,5 +16,5 @@ // NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php! -require_once(dir(__FILE__) . '/testing_data_generator.php'); -require_once(dir(__FILE__) . '/data_generator_base.php'); +require_once(__DIR__ . '/TestingDataGenerator.php'); +require_once(__DIR__ . '/DataGeneratorBase.php'); diff --git a/htdocs/testing/classes/generator/testing_data_generator.php b/htdocs/testing/classes/generator/testing_data_generator.php deleted file mode 100644 index a02a076b11c750853e827c49e377f1a6b942c851..0000000000000000000000000000000000000000 --- a/htdocs/testing/classes/generator/testing_data_generator.php +++ /dev/null @@ -1,102 +0,0 @@ -usercounter = 0; - $this->$groupcount = 0; - $this->$institutioncount = 0; - - foreach ($this->generators as $generator) { - $generator->reset(); - } - } - - /** - * Return generator for given plugin. - * @param string $plugintype the plugin type, e.g. 'artefact' or 'blocktype'. - * @param string $pluginname the plugin name, e.g. 'blog' or 'file'. - * @return an instance of a plugin generator extending from CoreGenerator. - */ - public function get_plugin_generator($plugintype, $pluginname) { - $pluginfullname = "{$plugintype}.{$pluginname}"; - if (isset($this->generators[$pluginfullname])) { - return $this->generators[$pluginfullname]; - } - safe_require($plugintype, $pluginname, 'tests/generator/lib.php'); - - $classname = generate_generator_class_name($plugintype, $pluginname); - - if (!class_exists($classname)) { - throw new SystemException("The plugin $pluginfullname does not support " . - "data generators yet. Class {$classname} not found."); - } - - $this->generators[$pluginfullname] = new $classname($this); - return $this->generators[$pluginfullname]; - - } - - /** - * Create a test user - * @param array|stdClass $record - * @param array $options - * @return stdClass user record - */ - public function create_user($record=null) { - } -} diff --git a/htdocs/testing/classes/util.php b/htdocs/testing/classes/util.php index 101e6a39fce3fa8af37092ee7dee38160d19c8d8..0229fc45cd5fa28256e4f2138c129c5fca7521b2 100644 --- a/htdocs/testing/classes/util.php +++ b/htdocs/testing/classes/util.php @@ -118,7 +118,7 @@ abstract class TestingUtil { */ public static function get_data_generator() { if (is_null(self::$generator)) { - require_once(dir(__FILE__) . '/generator/lib.php'); + require_once(__DIR__ . '/generator/lib.php'); self::$generator = new TestingDataGenerator(); } return self::$generator; @@ -155,8 +155,6 @@ abstract class TestingUtil { * @return bool */ public static function is_test_data_updated() { - global $CFG; - $framework = self::get_framework(); $datarootpath = self::get_dataroot() . '/' . $framework; @@ -195,16 +193,17 @@ abstract class TestingUtil { // store data for all tables $data = array(); $structure = array(); - $tables = get_tables(); + $tables = get_tables_from_xmldb(); foreach ($tables as $table) { - $columns = get_columns($table); - $structure[$table] = $columns; - if (isset($columns['id']) && $columns['id']->Auto_increment) { - $data[$table] = get_records_sql_array('SELECT * FROM ' . db_quote_identifier($table) . ' ORDER BY id ASC', array()); + $tablename = $table->getName(); + $columns = get_columns($tablename); + $structure[$tablename] = $columns; + if (isset($columns['ID']) && $columns['ID']->auto_increment) { + $data[$tablename] = get_records_array($tablename, '', '', 'id ASC'); } else { // there should not be many of these - $data[$table] = get_records_sql_array('SELECT * FROM ' . db_quote_identifier($table), array()); + $data[$tablename] = get_records_array($tablename); } } $data = serialize($data); @@ -314,51 +313,65 @@ abstract class TestingUtil { } /** - * Returns list of tables that are unmodified and empty. + * Returns list of tables that are unmodified or empty. * * @static * @return array of table names, empty if unknown */ protected static function guess_unmodified_empty_tables() { - $empties = array(); + $data = self::get_tabledata(); + $structure = self::get_tablestructure(); + $prefix = get_config('dbprefix'); + $unmodifiedorempties = array(); if (is_mysql()) { - $prefix = get_config('dbprefix'); $records = get_records_sql_array("SHOW TABLE STATUS LIKE ?", array($prefix . '%')); foreach ($records as $info) { - $table = strtolower($info->Name); - if (strpos($table, $prefix) !== 0) { + $tablename = strtolower($info->Name); + if (strpos($tablename, $prefix) !== 0) { // incorrect table match caused by _ continue; } - if (!empty($info->Auto_increment)) { - $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table); - if ($info->Auto_increment === 1) { - $empties[$table] = $table; + if (!empty($info->auto_increment)) { + $tablename = substr($tablename, strlen($prefix)); + if ($info->auto_increment === 1) { + $unmodifiedorempties[$tablename] = $tablename; } } } unset($records); } else if (is_postgres()) { - $tables = get_tables(); + $tables = get_tables_from_xmldb(); foreach ($tables as $table) { - $columns = get_columns($table); - if (isset($columns['id']) && isset($columns['id']->Auto_increment) && $columns['id']->Auto_increment === 1) { - $empties[$table] = $table; + $tablename = $table->getName(); + $columns = get_columns($tablename); + if (!record_exists($tablename) && empty($data[$tablename])) { + $unmodifiedorempties[$tablename] = $tablename; + continue; + } + if (isset($columns['ID']) && isset($columns['ID']->auto_increment)) { + if ($columns['ID']->auto_increment == 1) { + $unmodifiedorempties[$tablename] = $tablename; + } + else { + if (isset($structure[$tablename]['ID']->auto_increment) && $columns['ID']->auto_increment == $structure[$tablename]['ID']->auto_increment) { + $unmodifiedorempties[$tablename] = $tablename; + } + } } } } - return $empties; + return $unmodifiedorempties; } /** * Reset all database sequences to initial values. * * @static - * @param array $empties tables that are known to be unmodified and empty + * @param array $unmodifiedorempties tables that are known to be unmodified or empty * @return void */ - public static function reset_all_database_sequences(array $empties = null) { + public static function reset_all_database_sequences(array $unmodifiedorempties = null) { if (!$data = self::get_tabledata()) { // Not initialised yet. return; @@ -373,12 +386,13 @@ abstract class TestingUtil { if (is_postgres()) { $queries = array(); foreach ($data as $table => $records) { - if (isset($structure[$table]['id']) - && !empty($structure[$table]['id']->Auto_increment) + if (isset($structure[$table]['ID']) + && !empty($structure[$table]['ID']->auto_increment) ) { if (empty($records)) { $nextid = 1; - } else { + } + else { $lastrecord = end($records); $nextid = $lastrecord->id + 1; } @@ -399,14 +413,14 @@ abstract class TestingUtil { // incorrect table match caused by _ continue; } - if (!empty($info->Auto_increment)) { + if (!empty($info->auto_increment)) { $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table); - $sequences[$table] = $info->Auto_increment; + $sequences[$table] = $info->auto_increment; } } unset($records); foreach ($data as $table => $records) { - if (isset($structure[$table]['id']) && $structure[$table]['id']->Auto_increment) { + if (isset($structure[$table]['ID']) && isset($structure[$table]['ID']->auto_increment)) { if (isset($sequences[$table])) { if (empty($records)) { $nextid = 1; @@ -417,6 +431,7 @@ abstract class TestingUtil { } if ($sequences[$table] != $nextid) { execute_sql("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid"); + log_info('SQL command: ' . "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid"); } } @@ -433,10 +448,10 @@ abstract class TestingUtil { * @return bool true if reset done, false if skipped */ public static function reset_database() { - $tables = get_tables(); + $tables = get_tables_from_xmldb(); $prefix = get_config('dbprefix'); - if (empty($tables) || !isset($tables["{$prefix}config"])) { + if (!table_exists(new XMLDBTable('config'))) { // not installed yet return false; } @@ -450,135 +465,60 @@ abstract class TestingUtil { return false; } - $empties = self::guess_unmodified_empty_tables(); + $unmodifiedorempties = self::guess_unmodified_empty_tables(); db_begin(); - $brokedmysql = false; + // Temporary drop current foreign key contraints + $foreignkeys = array(); + foreach ($tables as $table) { + $tablename = $table->getName(); + $foreignkeys = array_merge($foreignkeys, get_foreign_keys($tablename)); + } + // Drop foreign key contraints if (is_mysql()) { - $serverinfo = get_server_info(); - $version = $serverinfo['version']; - if (version_compare($version, '5.6.0') == 1 and version_compare($version, '5.6.16') == -1) { - // Everything that comes from Oracle is evil! - // - // See http://dev.mysql.com/doc/refman/5.6/en/alter-table.html - // You cannot reset the counter to a value less than or equal to to the value that is currently in use. - // - // From 5.6.16 release notes: - // InnoDB: The ALTER TABLE INPLACE algorithm would fail to decrease the auto-increment value. - // (Bug #17250787, Bug #69882) - $brokedmysql = true; - - } else if (version_compare($version, '10.0.0') == 1) { - // And MariaDB is no better! - // Let's hope they pick the patch sometime later... - $brokedmysql = true; + foreach ($foreignkeys as $key) { + execute_sql('ALTER TABLE ' . db_quote_identifier($key['table']) . ' DROP FOREIGN KEY ' . db_quote_identifier($key['constraintname'])); } } - - if ($brokedmysql) { - $mysqlsequences = array(); - $records = get_records_sql_array("SHOW TABLE STATUS LIKE ?", array($prefix . '%')); - foreach ($records as $info) { - $table = strtolower($info->Name); - if (strpos($table, $prefix) !== 0) { - // incorrect table match caused by _ - continue; - } - if (!is_null($info->Auto_increment)) { - $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table); - $mysqlsequences[$table] = $info->Auto_increment; - } + else { + foreach ($foreignkeys as $key) { + execute_sql('ALTER TABLE ' . db_quote_identifier($key['table']) . ' DROP CONSTRAINT IF EXISTS ' . db_quote_identifier($key['constraintname'])); } - unset($records); } - - // Temporary disable foreign key check - if (is_mysql()) { - execute_sql('SET foreign_key_checks = 0'); - } - foreach ($data as $table => $records) { - if (is_postgres()) { - execute_sql('ALTER TABLE ' . db_quote_identifier($table) . 'DISABLE TRIGGER ALL'); - } - if ($brokedmysql) { - if (empty($records) && isset($empties[$table])) { - continue; - } - - if (isset($structure[$table]['id']) && $structure[$table]['id']->Auto_increment) { - $current = get_records_sql_array('SELECT * FROM ' . db_quote_identifier($table) . ' ORDER BY id ASC', array()); - if ($current == $records) { - if (isset($mysqlsequences[$table]) && $mysqlsequences[$table] == $structure[$table]['id']->Auto_increment) { - continue; - } - } - } - - // Empty the table and reinsert everything. - execute_sql('DELETE FROM ' . db_quote_identifier($table)); - foreach ($records as $record) { - insert_record(substr($table, strlen($prefix)), $record); - } + foreach ($tables as $table) { + $tablename = $table->getName(); + if (isset($unmodifiedorempties[$tablename])) { continue; } - - if (empty($records)) { - if (isset($empties[$table])) { - // table was not modified and is empty - } - else { - execute_sql('DELETE FROM ' . db_quote_identifier($table)); - } + if (!isset($data[$tablename])) { continue; } - - if (isset($structure[$table]['id']) and $structure[$table]['id']->Auto_increment) { - $currentrecords = get_records_sql_array('SELECT * FROM ' . db_quote_identifier($table) . ' ORDER BY id ASC', array()); - $changed = false; - foreach ($records as $id => $record) { - if (!isset($currentrecords[$id])) { - $changed = true; - break; - } - if ((array)$record != (array)$currentrecords[$id]) { - $changed = true; - break; - } - unset($currentrecords[$id]); - } - if (!$changed) { - if ($currentrecords) { - $lastrecord = end($records); - execute_sql('DELETE FROM ' . db_quote_identifier($table) . ' WHERE id > ?', array($lastrecord->id)); + // Empty the table + execute_sql('DELETE FROM {' . $tablename . '}'); + // Restore the table from the backup file + if ($data[$tablename]) { + foreach ($data[$tablename] as $record) { + insert_record($tablename, $record); + if ($tablename == 'usr' && $record->username == 'root' && is_mysql()) { + // gratuitous mysql workaround + set_field('usr', 'id', 0, 'username', 'root'); + execute_sql('ALTER TABLE {usr} AUTO_INCREMENT=1'); } - continue; } } - - execute_sql('DELETE FROM ' . db_quote_identifier($table)); - foreach ($records as $record) { - insert_record(substr($table, strlen($prefix)), $record); - } - if (is_postgres()) { - execute_sql('ALTER TABLE ' . db_quote_identifier($table) . 'ENABLE TRIGGER ALL'); - } } - // Enable foreign key check - if (is_mysql()) { - execute_sql('SET foreign_key_checks = 1'); - } + // Re-add foreign key contraints + foreach ($foreignkeys as $key) { + execute_sql('ALTER TABLE ' . db_quote_identifier($key['table']) . ' ADD CONSTRAINT ' + . db_quote_identifier($key['constraintname']) .' FOREIGN KEY ' + . '(' . implode(',', array_map('db_quote_identifier', $key['fields'])) . ')' + . ' REFERENCES ' . db_quote_identifier($key['reftable']) . '(' . implode(',', array_map('db_quote_identifier', $key['reffields'])) . ')'); + } db_commit(); // reset all next record ids - aka sequences - self::reset_all_database_sequences($empties); - - // remove extra tables - foreach ($tables as $table) { - if (!isset($data[$table])) { - drop_table(new XMLDBTable(substr($table, strlen($prefix)))); - } - } + self::reset_all_database_sequences($unmodifiedorempties); return true; } @@ -745,18 +685,20 @@ abstract class TestingUtil { execute_sql('ALTER TABLE {usr} DROP CONSTRAINT {usr_pro_fk}'); execute_sql('ALTER TABLE {institution} DROP CONSTRAINT {inst_log_fk}'); } - else { - execute_sql('ALTER TABLE {usr} DROP FOREIGN KEY {usr_pro_fk}'); - execute_sql('ALTER TABLE {institution} DROP FOREIGN KEY {inst_log_fk}'); - } } catch (Exception $e) { exit(1); } // now uninstall core + if (is_mysql()) { + execute_sql('SET foreign_key_checks = 0'); + } log_info('Uninstalling core'); uninstall_from_xmldb_file(get_config('docroot') . 'lib/db/install.xml'); + if (is_mysql()) { + execute_sql('SET foreign_key_checks = 1'); + } } @@ -833,7 +775,8 @@ abstract class TestingUtil { foreach (new RecursiveIteratorIterator($directory) as $file) { if ($file->isDir()) { $key = substr($file->getPath(), strlen(self::get_dataroot() . '/')); - } else { + } + else { $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/')); } $listfiles[$key] = $key; diff --git a/htdocs/testing/frameworks/behat/classes/BehatBase.php b/htdocs/testing/frameworks/behat/classes/BehatBase.php index 8f04ab8c89589b494f1606ce006f3241a06b9a42..b01740f50546c73b05af9f750227afd3e368ca0d 100644 --- a/htdocs/testing/frameworks/behat/classes/BehatBase.php +++ b/htdocs/testing/frameworks/behat/classes/BehatBase.php @@ -10,22 +10,17 @@ */ /** - * Base class of all steps definitions. + * Behat base class for mahara step definitions. * + * All mahara step definitions should be extended from this class */ - use Behat\Mink\Exception\ExpectationException as ExpectationException, Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException, Behat\Mink\Element\NodeElement as NodeElement; /** - * Steps definitions base class. - * - * To extend by the steps definitions of the different Moodle components. - * - * It can not contain steps definitions to avoid duplicates, only utility - * methods shared between steps. + * Base class * * @method NodeElement find_field(string $locator) Finds a form element * @method NodeElement find_button(string $locator) Finds a form input submit element or a button @@ -57,7 +52,7 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { /** * The JS code to check that the page is ready. */ - const PAGE_READY_JS = '(is_page_ready) && (document.readyState === "complete")'; + const PAGE_READY_JS = '(document.readyState === "complete")'; /** * Locates url, based on provided path. @@ -116,7 +111,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { $locator[1] = html_entity_decode($locator[1], ENT_NOQUOTES); } - } else { + } + else { $exceptiontype = $selector; $exceptionlocator = $locator; } @@ -151,7 +147,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { // We are in the container node. if (strpos($union, '.') === 0) { $union = substr($union, 1); - } else if (strpos($union, '/') !== 0) { + } + else if (strpos($union, '/') !== 0) { // Adding the path separator in case it is not there. $union = '/' . $union; } @@ -266,7 +263,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { if ($microsleep) { // Will sleep 1/10th of a second by default for self::TIMEOUT seconds. $loops = $timeout * 10; - } else { + } + else { // Will sleep for self::TIMEOUT seconds. $loops = $timeout; } @@ -280,7 +278,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { if ($return = call_user_func($lambda, $this, $args)) { return $return; } - } catch (Exception $e) { + } + catch (Exception $e) { // We would use the first closure exception if no exception has been provided. if (!$exception) { $exception = $e; @@ -291,7 +290,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { if ($microsleep) { usleep(100000); - } else { + } + else { sleep(1); } } @@ -384,12 +384,12 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { protected function transform_selector($selectortype, $element) { // Here we don't know if an allowed text selector is being used. - $selectors = behat_selectors::get_allowed_selectors(); + $selectors = BehatSelectors::get_allowed_selectors(); if (!isset($selectors[$selectortype])) { throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession()); } - return behat_selectors::get_behat_selector($selectortype, $element, $this->getSession()); + return BehatSelectors::get_behat_selector($selectortype, $element, $this->getSession()); } /** @@ -405,7 +405,7 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { */ protected function transform_text_selector($selectortype, $element) { - $selectors = behat_selectors::get_allowed_text_selectors(); + $selectors = BehatSelectors::get_allowed_text_selectors(); if (empty($selectors[$selectortype])) { throw new ExpectationException('The "' . $selectortype . '" selector can not be used to select text nodes', $this->getSession()); } @@ -562,7 +562,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { // If there are no editors we don't need to wait. try { $this->find('css', '.mceEditor'); - } catch (ElementNotFoundException $e) { + } + catch (ElementNotFoundException $e) { return; } @@ -667,11 +668,13 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext { try { $jscode = 'return ' . self::PAGE_READY_JS; $ready = $this->getSession()->evaluateScript($jscode); - } catch (NoSuchWindow $nsw) { + } + catch (NoSuchWindow $nsw) { // We catch an exception here, in case we just closed the window we were interacting with. // No javascript is running if there is no window right? $ready = true; - } catch (UnknownError $e) { + } + catch (UnknownError $e) { $ready = true; } diff --git a/htdocs/testing/frameworks/behat/classes/BehatCommand.php b/htdocs/testing/frameworks/behat/classes/BehatCommand.php index 1fa9a91ab47eff332d3244f7f3c999e2248f389a..707a730dac9b8555eaa7bc4d1c44cfbb12f2aade 100644 --- a/htdocs/testing/frameworks/behat/classes/BehatCommand.php +++ b/htdocs/testing/frameworks/behat/classes/BehatCommand.php @@ -58,7 +58,7 @@ class BehatCommand { $separator = DIRECTORY_SEPARATOR; $exec = 'behat'; - return 'vendor' . $separator . 'bin' . $separator . $exec; + return $separator . 'vendor' . $separator . 'bin' . $separator . $exec; } /** @@ -73,8 +73,9 @@ class BehatCommand { global $CFG; $currentcwd = getcwd(); + // Change to composer installed directory chdir($CFG->docroot); - exec(self::get_behat_command() . ' ' . $options, $output, $code); + exec(get_composerroot_dir() . self::get_behat_command() . ' ' . $options . ' 2>/dev/null', $output, $code); chdir($currentcwd); return array($output, $code); @@ -150,7 +151,7 @@ class BehatCommand { * @return bool */ public static function are_behat_dependencies_installed() { - if (!is_dir(dirname(dirname(dirname(dirname(__DIR__)))) . '/vendor/behat')) { + if (!is_dir(get_composerroot_dir() . '/vendor/behat')) { return false; } return true; @@ -176,7 +177,8 @@ class BehatCommand { // Stopping execution. exit(1); - } else { + } + else { // We continue execution after this. $clibehaterrorstr = "Ensure you set \$CFG->behat_* vars in config.php " . diff --git a/htdocs/testing/frameworks/behat/classes/BehatConfigManager.php b/htdocs/testing/frameworks/behat/classes/BehatConfigManager.php index 7f0396df655344e8d0cb2e249613c73350c20e5a..e449fb77b2a72de56ebff77fa7f12f0b6d3becff 100644 --- a/htdocs/testing/frameworks/behat/classes/BehatConfigManager.php +++ b/htdocs/testing/frameworks/behat/classes/BehatConfigManager.php @@ -49,13 +49,15 @@ class BehatConfigManager { // Behat must have a separate behat.yml to have access to the whole set of features and steps definitions. if ($testsrunner === true) { $configfilepath = BehatCommand::get_behat_dir() . '/behat.yml'; - } else { + } + else { // Alternative for steps definitions filtering, one for each user. $configfilepath = self::get_steps_list_config_filepath(); } + // Get core features + $features = array(dirname(dirname(dirname(dirname(dirname(__DIR__))))) . '/test/behat/features'); // Gets all the plugins with features. - $features = array(); $plugins = TestsFinder::get_plugins_with_tests('features'); if ($plugins) { foreach ($plugins as $pluginname => $path) { @@ -67,7 +69,7 @@ class BehatConfigManager { $featurespaths[$uniquekey] = $path; } } - $features = array_values($featurespaths); + $features = array_merge($features, array_values($featurespaths)); } // Optionally include features from additional directories. @@ -75,8 +77,16 @@ class BehatConfigManager { $features = array_merge($features, array_map("realpath", $CFG->behat_additionalfeatures)); } - // Gets all the plugins with steps definitions. $stepsdefinitions = array(); + // Find step definitions from core. They must be in the folder $MAHARA_ROOT/test/behat/stepdefinitions + // The file name must be /^Behat[A-z0-9_]+\.php$/ + $regite = new RegexIterator(new DirectoryIterator(get_mahararoot_dir() . '/test/behat/stepdefinitions'), '|^Behat[A-z0-9_\-]+\.php$|'); + foreach ($regite as $file) { + $key = $file->getBasename('.php'); + $stepsdefinitions[$key] = $file->getPathname(); + } + + // Gets all the plugins with steps definitions. $steps = self::get_plugins_steps_definitions(); if ($steps) { foreach ($steps as $key => $filepath) { @@ -120,6 +130,7 @@ class BehatConfigManager { } $stepsdefinitions = array(); + // Find step definitions from plugins foreach ($plugins as $pluginname => $pluginpath) { $pluginpath = self::clean_path($pluginpath); @@ -127,7 +138,7 @@ class BehatConfigManager { continue; } $diriterator = new DirectoryIterator($pluginpath . self::get_behat_tests_path()); - $regite = new RegexIterator($diriterator, '|Behat.*\.php$|'); + $regite = new RegexIterator($diriterator, '|^Behat.*\.php$|'); // All Behat*.php inside BehatConfigManager::get_behat_tests_path() are added as steps definitions files. foreach ($regite as $file) { @@ -179,11 +190,11 @@ class BehatConfigManager { global $CFG; // We require here when we are sure behat dependencies are available. - require_once($CFG->docroot . '/vendor/autoload.php'); + require_once($CFG->docroot . '../external/vendor/autoload.php'); // It is possible that it has no value as we don't require a full behat setup to list the step definitions. if (empty($CFG->behat_wwwroot)) { - $CFG->behat_wwwroot = 'http://itwillnotbeused.com'; + $CFG->behat_wwwroot = 'http://example.com'; } $basedir = $CFG->docroot . 'testing' . DIRECTORY_SEPARATOR . 'frameworks' . DIRECTORY_SEPARATOR . 'behat'; @@ -199,6 +210,7 @@ class BehatConfigManager { 'extensions' => array( 'Behat\MinkExtension\Extension' => array( 'base_url' => $CFG->behat_wwwroot, + 'files_path' => get_mahararoot_dir() . '/test/behat/upload_files', 'goutte' => null, 'selenium2' => null ), @@ -254,7 +266,8 @@ class BehatConfigManager { // Add the param if it doesn't exists or merge branches. if (empty($config[$key])) { $config[$key] = $value; - } else { + } + else { $config[$key] = self::merge_config($config[$key], $localconfig[$key]); } } diff --git a/htdocs/testing/frameworks/behat/classes/BehatDataGenerators.php b/htdocs/testing/frameworks/behat/classes/BehatDataGenerators.php new file mode 100644 index 0000000000000000000000000000000000000000..e1e1b81276ae3784eccace86ab089637d58c112b --- /dev/null +++ b/htdocs/testing/frameworks/behat/classes/BehatDataGenerators.php @@ -0,0 +1,254 @@ +fieldtype) + * - The required fields. + * - The mapping between other elements references and database field names. + * @var array + */ + protected static $elements = array( + 'users' => array( + 'datagenerator' => 'user', + 'available' => array( + 'username' => 'text', + 'password' => 'text', + 'email' => 'text', + 'firstname' => 'text', + 'lastname' => 'text', + 'institution' => 'text', + 'role' => 'text', + 'authname' => 'text', + 'remoteusername' => 'text', + ), + 'required' => array('username', 'password', 'email', 'firstname', 'lastname') + ), + 'groups' => array( + 'datagenerator' => 'group', + 'available' => array( + 'name' => 'text', + 'owner' => 'text', + 'description' => 'text', + 'grouptype' => 'text', + 'open' => 'bool', + 'controlled' => 'bool', + 'request' => 'bool', + 'invitefriends' => 'bool', + 'suggestfriends' => 'bool', + 'editroles' => 'text', + 'submittableto' => 'bool', + 'allowarchives' => 'bool', + 'editwindowstart' => 'text', + 'editwindowend' => 'text', + 'members' => 'text', + 'staff' => 'text', + 'admins' => 'text', + ), + 'required' => array('name', 'owner'), + ), + 'institutions' => array( + 'datagenerator' => 'institution', + 'available' => array( + 'name' => 'text', + 'displayname' => 'text', + 'showonlineusers' => 'number', + 'registerallowed' => 'bool', + 'registerconfirm' => 'bool', + 'lang' => 'text', + 'theme' => 'text', + 'dropdownmenu' => 'bool', + 'skins' => 'bool', + 'licensemandatory' => 'bool', + 'licensedefault' => 'text', + 'defaultquota' => 'number', + 'defaultmembershipperiod' => 'number', + 'maxuseraccounts' => 'number', + 'expiry' => 'text', + 'allowinstitutionpublicviews' => 'bool', + 'members' => 'text', + 'staff' => 'text', + 'admins' => 'text', + ), + 'required' => array('name', 'displayname') + ), + 'group memberships' => array( + 'datagenerator' => 'group_membership', + 'required' => array('username', 'groupname', 'role') + ), + 'institution memberships' => array( + 'datagenerator' => 'institution_membership', + 'required' => array('username', 'institutionname', 'role') + ), + ); + + /** + * Normalise values in a given record + * For example, 'ON' -> 1, 'OFF' -> 0 + * @param array ('field' => 'values', ...) $record + * @return $record + */ + public function normalise(&$record) { + foreach ($record as &$value) { + $value = trim($value); + // Normalise boolean values + if (strtolower($value) == 'on' || $value == '1') { + $value = true; + } + else if (strtolower($value) == 'off' || $value == '0') { + $value = false; + } + } + } + + /** + * Validate field values in a given record + * + * @param array ('fieldname' => 'fieldtype', ...) $availablefields + * @param array ('fieldname' => 'values', ...) $record + * @return void + * @throws MaharaBehatTestException + */ + public function validate_fields($availablefields, $record) { + foreach ($record as $fieldname => $fieldvalue) { + if (!in_array($fieldname, array_keys($availablefields))) { + throw new MaharaBehatTestException("The field '" . $fieldname . "' is not available.\n". + "All available fields are " . implode(',', array_keys($availablefields))); + } + if ($availablefields[$fieldname] == 'bool' && !is_bool($fieldvalue)) { + throw new MaharaBehatTestException("The value '" . $fieldvalue . "' of the field '" . $fieldname . "' must be a boolean ('ON'|'OFF', '1'|'0' are accepted boolean values)."); + } + if ($availablefields[$fieldname] == 'number' && !is_numeric($fieldvalue)) { + throw new MaharaBehatTestException("The value '" . $fieldvalue . "' of the field '" . $fieldname . "' must be a number."); + } + } + } + + /** + * Creates the specified element. + * + * @Given /^the following "(?P' . $expectedvalue . '
'); + } +} + diff --git a/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormField.php b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormField.php new file mode 100644 index 0000000000000000000000000000000000000000..044d2abfb7482fc647cbcdacb4113de68dde565f --- /dev/null +++ b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormField.php @@ -0,0 +1,219 @@ +session = $session; + $this->field = $fieldnode; + } + + /** + * Sets the value to a field. + * + * @param string $value + * @return void + */ + public function set_value($value) { + // We delegate to the best guess, if we arrived here + // using the generic BehatFormField is because we are + // dealing with a fgroup element. + $instance = $this->guess_type(); + return $instance->set_value($value); + } + + /** + * Returns the current value of the select element. + * + * @return string + */ + public function get_value() { + // We delegate to the best guess, if we arrived here + // using the generic BehatFormField is because we are + // dealing with a fgroup element. + $instance = $this->guess_type(); + return $instance->get_value(); + } + + /** + * Generic match implementation + * + * Will work well with text-based fields, extension required + * for most of the other cases. + * + * @param string $expectedvalue + * @return bool The provided value matches the field value? + */ + public function matches($expectedvalue) { + // We delegate to the best guess, if we arrived here + // using the generic BehatFormField is because we are + // dealing with a fgroup element. + $instance = $this->guess_type(); + return $instance->matches($expectedvalue); + } + + /** + * Guesses the element type we are dealing with in case is not a text-based element. + * + * This class is the generic field type, BehatFieldManager::get_FormFields() + * should be able to find the appropiate class for the field type, but + * in cases like mahara form group elements we can not find the type of + * the field through the DOM so we also need to take care of the + * different field types from here. If we need to deal with more complex + * mahara form elements we will need to refactor this simple HTML elements + * guess method. + * + * @return BehatFormField + */ + private function guess_type() { + global $CFG; + + // We default to the text-based field if nothing was detected. + if (!$type = BehatFieldManager::guess_field_type($this->field, $this->session)) { + $type = 'text'; + } + + $classname = 'BehatForm' . ucfirst($type); + $classpath = __DIR__ . '/' . $classname . '.php'; + require_once($classpath); + return new $classname($this->session, $this->field); + } + + /** + * Returns whether the scenario is running in a browser that can run Javascript or not. + * + * @return bool + */ + protected function running_javascript() { + return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver'; + } + + /** + * Gets the field internal id used by selenium wire protocol. + * + * Only available when running_javascript(). + * + * @throws SystemException + * @return int + */ + protected function get_internal_field_id() { + + if (!$this->running_javascript()) { + throw new SystemException('You can only get an internal ID using the selenium driver.'); + } + + return $this->session->getDriver()->getWebDriverSession()->element('xpath', $this->field->getXPath())->getID(); + } + + /** + * Checks if the provided text matches the field value. + * + * @param string $expectedvalue + * @return bool + */ + protected function text_matches($expectedvalue) { + if (trim($expectedvalue) != trim($this->get_value())) { + return false; + } + return true; + } + + /** + * Gets the field locator. + * + * Defaults to the field label but you can + * specify other locators if you are interested. + * + * Public visibility as in most cases will be hard to + * use this method in a generic way, as fields can + * be selected using multiple ways (label, id, name...). + * + * @throws SystemException + * @param string $locatortype + * @return string + */ + protected function get_field_locator($locatortype = false) { + + if (!empty($this->fieldlocator)) { + return $this->fieldlocator; + } + + $fieldid = $this->field->getAttribute('id'); + + // Defaults to label. + if ($locatortype == 'label' || $locatortype == false) { + + $labelnode = $this->session->getPage()->find('xpath', '//label[@for="' . $fieldid . '"]'); + + // Exception only if $locatortype was specified. + if (!$labelnode && $locatortype == 'label') { + throw new SystemException('Field with "' . $fieldid . '" id does not have a label.'); + } + + $this->fieldlocator = $labelnode->getText(); + } + + // Let's look for the name as a second option (more popular than + // id's when pointing to fields). + if (($locatortype == 'name' || $locatortype == false) && + empty($this->fieldlocator)) { + + $name = $this->field->getAttribute('name'); + + // Exception only if $locatortype was specified. + if (!$name && $locatortype == 'name') { + throw new SystemException('Field with "' . $fieldid . '" id does not have a name attribute.'); + } + + $this->fieldlocator = $name; + } + + // Otherwise returns the id if no specific locator type was provided. + if (empty($this->fieldlocator)) { + $this->fieldlocator = $fieldid; + } + + return $this->fieldlocator; + } + +} diff --git a/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormFileManager.php b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormFileManager.php new file mode 100644 index 0000000000000000000000000000000000000000..934c1eb18aed589e0ff92effb4266bba5eb908ed --- /dev/null +++ b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormFileManager.php @@ -0,0 +1,106 @@ +session->wait(behat_base::TIMEOUT, behat_base::PAGE_READY_JS); + + // Get the label to restrict the files to this single form field. + $fieldlabel = $this->get_field_locator(); + + // Get the name of the current directory elements. + $xpath = "//label[contains(., '" . $fieldlabel . "')]" . + "/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' fitemtitle ')]" . + "/following-sibling::div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ')]" . + "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename ')]"; + + // We don't need to wait here, also we don't have access to protected + // contexts find* methods. + $files = $this->session->getPage()->findAll('xpath', $xpath); + + if (!$files) { + return ''; + } + + $filenames = array(); + foreach ($files as $filenode) { + $filenames[] = $filenode->getText(); + } + + return implode(',', $filenames); + } + + /** + * Sets the field value. + * + * @param string $value + * @return void + */ + public function set_value($value) { + + // Getting the filemanager label from the DOM. + $fieldlabel = $this->get_field_locator(); + + // Getting the filepicker context and using the step definition + // to upload the requested file. + $uploadcontext = BehatContextHelper::get('behat_repository_upload'); + $uploadcontext->i_upload_file_to_filemanager($value, $fieldlabel); + } + + /** + * Matches the provided filename/s against the current field value. + * + * If the filemanager contains more than one file the $expectedvalue + * value should include all the file names separating them by comma. + * + * @param string $expectedvalue + * @return bool The provided value matches the field value? + */ + public function matches($expectedvalue) { + return $this->text_matches($expectedvalue); + } + +} diff --git a/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormRadio.php b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormRadio.php new file mode 100644 index 0000000000000000000000000000000000000000..3bdc1d993ea17862f2ec10269b33319cb3c8f3e2 --- /dev/null +++ b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormRadio.php @@ -0,0 +1,75 @@ +field->getAttribute('checked'); + } + + /** + * Sets the value of a radio + * + * Partially overwriting BehatFormCheckbox + * implementation as when JS is disabled we + * can not check() and we should use setValue() + * + * @param string $value + * @return void + */ + public function set_value($value) { + + if ($this->running_javascript()) { + parent::set_value($value); + } + else { + // Goutte does not accept a check nor a click in an input[type=radio]. + $this->field->setValue($this->field->getAttribute('value')); + } + } + + /** + * Returns whether the provided value matches the current value or not. + * + * @param string $expectedvalue + * @return bool + */ + public function matches($expectedvalue = false) { + return $this->text_matches($expectedvalue); + } +} diff --git a/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormSelect.php b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormSelect.php new file mode 100644 index 0000000000000000000000000000000000000000..65547b6e03dac4e1865f863ad199dfe84dfec139 --- /dev/null +++ b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormSelect.php @@ -0,0 +1,337 @@ +running_javascript()) { + $currentelementid = $this->get_internal_field_id(); + } + + // Is the select multiple? + $multiple = $this->field->hasAttribute('multiple'); + + // By default, assume the passed value is a non-multiple option. + $options = array(trim($value)); + + // Here we select the option(s). + if ($multiple) { + // Split and decode values. Comma separated list of values allowed. With valuable commas escaped with backslash. + $options = preg_replace('/\\\,/', ',', preg_split('/(?field->selectOption(trim($option), $afterfirstoption); + $afterfirstoption = true; + } + } + else { + // This is a single select, let's pass the last one specified. + $this->field->selectOption(end($options)); + } + + // With JS disabled this is enough and we finish here. + if (!$this->running_javascript()) { + return; + } + + // With JS enabled we add more clicks as some selenium + // drivers requires it to fire JS events. + + // In some browsers the selectOption actions can perform a form submit or reload page + // so we need to ensure the element is still available to continue interacting + // with it. We don't wait here. + // getXpath() does not send a query to selenium, so we don't need to wrap it in a try & catch. + $selectxpath = $this->field->getXpath(); + if (!$this->session->getDriver()->find($selectxpath)) { + return; + } + + // We also check the selenium internal element id, if it have changed + // we are dealing with an autosubmit that was already executed, and we don't to + // execute anything else as the action we wanted was already performed. + if ($currentelementid != $this->get_internal_field_id()) { + return; + } + + // Wait for all the possible AJAX requests that have been + // already triggered by selectOption() to be finished. + $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS); + + // Wrapped in try & catch as the element may disappear if an AJAX request was submitted. + try { + $multiple = $this->field->hasAttribute('multiple'); + } + catch (Exception $e) { + // We do not specify any specific Exception type as there are + // different exceptions that can be thrown by the driver and + // we can not control them all, also depending on the selenium + // version the exception type can change. + return; + } + + // Single select sometimes needs an extra click in the option. + if (!$multiple) { + + // Var $options only contains 1 option. + $optionxpath = $this->get_option_xpath(end($options), $selectxpath); + + // Using the driver direcly because Element methods are messy when dealing + // with elements inside containers. + if ($optionnodes = $this->session->getDriver()->find($optionxpath)) { + + // Wrapped in a try & catch as we can fall into race conditions + // and the element may not be there. + try { + current($optionnodes)->click(); + } + catch (Exception $e) { + // We continue and return as this means that the element is not there or it is not the same. + return; + } + } + + } + else { + + // Wrapped in a try & catch as we can fall into race conditions + // and the element may not be there. + try { + // Multiple ones needs the click in the select. + $this->field->click(); + } + catch (Exception $e) { + // We continue and return as this means that the element is not there or it is not the same. + return; + } + + // We also check that the option(s) are still there. We neither wait. + foreach ($options as $option) { + $optionxpath = $this->get_option_xpath($option, $selectxpath); + if (!$this->session->getDriver()->find($optionxpath)) { + return; + } + } + + // Wait for all the possible AJAX requests that have been + // already triggered by clicking on the field to be finished. + $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS); + + // Wrapped in a try & catch as we can fall into race conditions + // and the element may not be there. + try { + + // Repeating the select(s) as some drivers (chrome that I know) are moving + // to another option after the general select field click above. + $afterfirstoption = false; + foreach ($options as $option) { + $this->field->selectOption(trim($option), $afterfirstoption); + $afterfirstoption = true; + } + } + catch (Exception $e) { + // We continue and return as this means that the element is not there or it is not the same. + return; + } + } + } + + /** + * Returns the text of the currently selected options. + * + * @return string Comma separated if multiple options are selected. Commas in option texts escaped with backslash. + */ + public function get_value() { + return $this->get_selected_options(); + } + + /** + * Returns whether the provided argument matches the current value. + * + * @param mixed $expectedvalue + * @return bool + */ + public function matches($expectedvalue) { + + $multiple = $this->field->hasAttribute('multiple'); + + // Same implementation as the parent if it is a single select. + if (!$multiple) { + $cleanexpectedvalue = trim($expectedvalue); + $selectedtext = trim($this->get_selected_options()); + $selectedvalue = trim($this->get_selected_options(false)); + if ($cleanexpectedvalue != $selectedvalue && $cleanexpectedvalue != $selectedtext) { + return false; + } + return true; + } + + // We are dealing with a multi-select. + + // Can pass multiple comma separated, with valuable commas escaped with backslash. + $expectedarr = array(); // Array of passed text options to test. + + // Unescape + trim all options and flip it to have the expected values as keys. + $expectedoptions = $this->get_unescaped_options($expectedvalue); + + // Get currently selected option's texts. + $texts = $this->get_selected_options(true); + $selectedoptiontexts = $this->get_unescaped_options($texts); + + // Get currently selected option's values. + $values = $this->get_selected_options(false); + $selectedoptionvalues = $this->get_unescaped_options($values); + + // Precheck to speed things up. + if (count($expectedoptions) !== count($selectedoptiontexts) || + count($expectedoptions) !== count($selectedoptionvalues)) { + return false; + } + + // We check against string-ordered lists of options. + if ($expectedoptions != $selectedoptiontexts && + $expectedoptions != $selectedoptionvalues) { + return false; + } + + return true; + } + + /** + * Cleans the list of options and returns it as a string separating options with |||. + * + * @param string $value The string containing the escaped options. + * @return string The options + */ + protected function get_unescaped_options($value) { + + // Can be multiple comma separated, with valuable commas escaped with backslash. + $optionsarray = array_map( + 'trim', + preg_replace('/\\\,/', ',', + preg_split('/(?field->hasAttribute('multiple'); + + $selectedoptions = array(); // To accumulate found selected options. + + // Selenium getValue() implementation breaks - separates - values having + // commas within them, so we'll be looking for options with the 'selected' attribute instead. + if ($this->running_javascript()) { + // Get all the options in the select and extract their value/text pairs. + $alloptions = $this->field->findAll('xpath', '//option'); + foreach ($alloptions as $option) { + // Is it selected? + if ($option->hasAttribute('selected')) { + if ($multiple) { + // If the select is multiple, text commas must be encoded. + $selectedoptions[] = trim(str_replace(',', '\,', $option->{$method}())); + } + else { + $selectedoptions[] = trim($option->{$method}()); + } + } + } + + } + else { + // Goutte does not keep the 'selected' attribute updated, but its getValue() returns + // the selected elements correctly, also those having commas within them. + + // Goutte returns the values as an array or as a string depending + // on whether multiple options are selected or not. + $values = $this->field->getValue(); + if (!is_array($values)) { + $values = array($values); + } + + // Get all the options in the select and extract their value/text pairs. + $alloptions = $this->field->findAll('xpath', '//option'); + foreach ($alloptions as $option) { + // Is it selected? + if (in_array($option->getValue(), $values)) { + if ($multiple) { + // If the select is multiple, text commas must be encoded. + $selectedoptions[] = trim(str_replace(',', '\,', $option->{$method}())); + } + else { + $selectedoptions[] = trim($option->{$method}()); + } + } + } + } + + return implode(', ', $selectedoptions); + } + + /** + * Returns the opton XPath based on it's select xpath. + * + * @param string $option + * @param string $selectxpath + * @return string xpath + */ + protected function get_option_xpath($option, $selectxpath) { + $valueliteral = $this->session->getSelectorsHandler()->xpathLiteral(trim($option)); + return $selectxpath . "/descendant::option[(./@value=$valueliteral or normalize-space(.)=$valueliteral)]"; + } +} diff --git a/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormText.php b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormText.php new file mode 100644 index 0000000000000000000000000000000000000000..6883605369e08dced7526e64856b200363ae1dee --- /dev/null +++ b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormText.php @@ -0,0 +1,49 @@ +field->setValue($value); + } + + /** + * Returns the current value of the element. + * + * @return string + */ + public function get_value() { + return $this->field->getValue(); + } + + /** + * Matches the provided value against the current field value. + * + * @param string $expectedvalue + * @return bool The provided value matches the field value? + */ + public function matches($expectedvalue) { + return $this->text_matches($expectedvalue); + } + +} diff --git a/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormTextarea.php b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormTextarea.php new file mode 100644 index 0000000000000000000000000000000000000000..edd385dbbab630434eaeed4d47cf6572e7799b6f --- /dev/null +++ b/htdocs/testing/frameworks/behat/classes/FormFields/BehatFormTextarea.php @@ -0,0 +1,19 @@ +description = 'Get behat test environment status code'; $options['diag']->required = false; $options['diag']->defaultvalue = false; +$options['config'] = new stdClass(); +$options['config']->shortoptions = array('c'); +$options['config']->description = 'Get behat YML config path'; +$options['config']->required = false; +$options['config']->defaultvalue = false; + $settings = new stdClass(); $settings->options = $options; $settings->info = 'CLI tool to manage Behat integration in Mahara'; @@ -103,6 +109,13 @@ try { $code = BehatTestingUtil::get_behat_status(); exit($code); } + else if ($cli->get_cli_param('config')) { + $code = BehatTestingUtil::get_behat_status(); + if ($code == 0) { + echo BehatTestingUtil::get_behat_config_path(); + } + exit($code); + } } catch (Exception $e) { cli::cli_exit($e->getMessage(), true); diff --git a/htdocs/testing/frameworks/behat/features/bootstrap/BehatMaharaInitContext.php b/htdocs/testing/frameworks/behat/features/bootstrap/BehatMaharaInitContext.php index a44671bdc44c3dee7ad506970783bec1af9061ea..37645c94aee1f1f26286b0a90de30b01cc5da238 100644 --- a/htdocs/testing/frameworks/behat/features/bootstrap/BehatMaharaInitContext.php +++ b/htdocs/testing/frameworks/behat/features/bootstrap/BehatMaharaInitContext.php @@ -14,6 +14,9 @@ * */ require_once(dirname(dirname(__DIR__)) . '/classes/BehatHooks.php'); +require_once(dirname(dirname(__DIR__)) . '/classes/BehatGeneral.php'); +require_once(dirname(dirname(__DIR__)) . '/classes/BehatForms.php'); +require_once(dirname(dirname(__DIR__)) . '/classes/BehatDataGenerators.php'); use Behat\Behat\Context\BehatContext, Behat\MinkExtension\Context\MinkContext; @@ -29,6 +32,9 @@ class BehatMaharaInitContext extends MinkContext { public function __construct(array $parameters) { // Initialize must have subcontexts $this->useContext('BehatHooks', new BehatHooks($parameters)); + $this->useContext('BehatGeneral', new BehatGeneral($parameters)); + $this->useContext('BehatForms', new BehatForms($parameters)); + $this->useContext('BehatDataGenerators', new BehatDataGenerators($parameters)); $this->useContext('mahara', new MaharaContext($parameters)); } diff --git a/htdocs/testing/frameworks/behat/features/extensions/MaharaExtension.php b/htdocs/testing/frameworks/behat/features/extensions/MaharaExtension.php index 21071adffbe86ef95deba40d13bf68ff6a14ad94..478f298be672c8bbdbaa3fa6a090abbae6f22481 100644 --- a/htdocs/testing/frameworks/behat/features/extensions/MaharaExtension.php +++ b/htdocs/testing/frameworks/behat/features/extensions/MaharaExtension.php @@ -192,7 +192,7 @@ class MaharaGherkin extends Gherkin { throw new RuntimeException('There are no Mahara features nor steps definitions'); } - // Loads all the features files of each Mahara plugin. + // Loads all the features files of Mahara core and plugins. $features = array(); if (!empty($this->maharaConfig['features'])) { foreach ($this->maharaConfig['features'] as $path) { diff --git a/htdocs/testing/frameworks/behat/lib.php b/htdocs/testing/frameworks/behat/lib.php index e13b09fd20ac0088242eb4c2cec985fef4385b25..729a31b07479692afb3c6ebded83a14b2145b24c 100644 --- a/htdocs/testing/frameworks/behat/lib.php +++ b/htdocs/testing/frameworks/behat/lib.php @@ -261,14 +261,16 @@ function behat_is_requested_url($url) { $pos = strpos($_SERVER['HTTP_HOST'], ':'); if ($pos !== false) { $requestedhost = substr($_SERVER['HTTP_HOST'], 0, $pos); - } else { + } + else { $requestedhost = $_SERVER['HTTP_HOST']; } // The path should also match. if (empty($parsedurl['path'])) { $matchespath = true; - } else if (strpos($_SERVER['SCRIPT_NAME'], $parsedurl['path']) === 0) { + } + else if (strpos($_SERVER['SCRIPT_NAME'], $parsedurl['path']) === 0) { $matchespath = true; } diff --git a/htdocs/testing/lib.php b/htdocs/testing/lib.php index 6a7a34bd9c028d537978fcaac4f509054409e80f..6464e30c0d7b70fbd864e502bad419ca5fc5c53a 100644 --- a/htdocs/testing/lib.php +++ b/htdocs/testing/lib.php @@ -15,6 +15,22 @@ * */ +/** + * Return mahara root directory + * @return string Full path + */ +function get_mahararoot_dir() { + return dirname(dirname(__DIR__)); +} + +/** + * Return composer root directory + * @return string Full path + */ +function get_composerroot_dir() { + return get_mahararoot_dir() . '/external'; +} + /** * Returns relative path against current working directory, * to be used for shell execution hints. @@ -27,7 +43,8 @@ function testing_cli_argument_path($maharapath) { if (isset($_SERVER['REMOTE_ADDR'])) { // Web access, this should not happen often. $cwd = dirname(dirname(__DIR__)); - } else { + } + else { // This is the real CLI script, work with relative paths. $cwd = getcwd(); } @@ -110,12 +127,12 @@ function testing_update_composer_dependencies() { // To restore the value after finishing. $cwd = getcwd(); - // Mahara Docroot. - $maharadocroot = dirname(__DIR__); - chdir($maharadocroot); + // Directory to install PHP composer + $composerroot = get_composerroot_dir(); + chdir($composerroot); // Download composer.phar if we can. - if (!file_exists($maharadocroot . '/composer.phar')) { + if (!file_exists($composerroot . '/composer.phar')) { passthru("curl http://getcomposer.org/installer | php", $code); if ($code != 0) { exit($code); diff --git a/htdocs/view/layout.php b/htdocs/view/layout.php index a722611c04b9c72db79afe9df1fcf891c8ac62fc..f2a3ca81ad758b256eb0a54d9658caafcffc7313 100644 --- a/htdocs/view/layout.php +++ b/htdocs/view/layout.php @@ -92,7 +92,7 @@ foreach ($columnlayouts as $layout => $percents) { } // provide a simple default to build custom layouts with -$defaultcustomlayout = $view->default_columnsperrow(); +$defaultcustomlayout = View::default_columnsperrow(); $defaultlayout = get_record('view_layout_columns', 'columns', $defaultcustomlayout[1]->columns, 'widths', $defaultcustomlayout[1]->widths); $clnumcolumnsdefault = $defaultlayout->columns; $clwidths = $defaultlayout->widths; diff --git a/test/behat/features/EditProfile.feature b/test/behat/features/EditProfile.feature new file mode 100644 index 0000000000000000000000000000000000000000..7d9b8087f27f7df9363e6b6e83d539716ac65a38 --- /dev/null +++ b/test/behat/features/EditProfile.feature @@ -0,0 +1,50 @@ +@javascript @core @profile +Feature: Mahara users can have their profile + As a mahara user + I can manage my profile + Background: + Given the following "users" exist: + | username | password | email | firstname | lastname | institution | authname | role | + | userA | Password1 | test01@example.com | Pete | Mc | mahara | internal | member | + | userB | Password1 | test02@example.com | Son | Nguyen | mahara | internal | member | + | userC | Password1 | test03@example.com | Jack | Smith | mahara | internal | member | + | userD | Password1 | test04@example.com | Eden | Wilson | mahara | internal | member | + | userE | Password1 | test05@example.com | Emily | Pham | mahara | internal | member | + And the following "institutions" exist: + | name | displayname | registerallowed | registerconfirm | members | staff | admins | + | instone | Institution One | ON | OFF | userB | | userA | + | insttwo | Institution Two | ON | ON | userB,userC | userD | userA | + And the following "groups" exist: + | name | owner | description | grouptype | open | invitefriends | editroles | submittableto | allowarchives | members | staff | + | group 01 | userA | This is group 01 | standard | ON | ON | all | ON | ON | userB, userC | userD | + @javascript + Scenario: Edit Profile + Given I log in as "userE" with password "Password1" + When I click on "Content" + Then I should see "Profile" + And the "firstname" field should contain "Emily" + When I fill in "firstname" with "Tiger" + When I fill in "lastname" with "Wood" + When I fill in "studentid" with "1234" + When I fill in "preferredname" with "Golf Legend" + When I click on "Contact information" + Then I should see "Email address" + And I should see "Official website address" + When I fill in "officialwebsite" with "www.catalyst.net.nz" + And I fill in "personalwebsite" with "www.stuff.co.nz" + When I fill in "blogaddress" with "www.blog.com" + When I fill in "profileform_address" with "150 Willis Street" + When I fill in "city" with "CBD" + When I fill in "homenumber" with "04928375" + When I fill in "businessnumber" with "040298375" + When I fill in "mobilenumber" with "0272093875482" + When I fill in "faxnumber" with "09237842" + When I click on "Social media" + Then I should see "Social network" + When I click on "General" + Then I should see "Occupation" + When I fill in "occupation" with "Software Engineer" + When I fill in "industry" with "it" + When I press "Save profile" + When I wait until the page is ready + Then I should see "Profile saved successfully" diff --git a/htdocs/account/tests/behat/ChangeAccountSettings.feature b/test/behat/features/account/ChangeAccountSettings.feature similarity index 60% rename from htdocs/account/tests/behat/ChangeAccountSettings.feature rename to test/behat/features/account/ChangeAccountSettings.feature index 99d423b1672fb0401a7de3d551749eb49db1565f..023eb016fb4809e73b76ba92d44683e9bbed5a5d 100644 --- a/htdocs/account/tests/behat/ChangeAccountSettings.feature +++ b/test/behat/features/account/ChangeAccountSettings.feature @@ -3,25 +3,25 @@ Feature: Mahara users can change their account settings As a mahara user I need to change my account settings - Scenario: Change password + Background: Given the following "users" exist: - | user | password | institution | role | - | userA | Password1 | mahara | member | - And I log in as "userA" with password "Password1" + | username | password | email | firstname | lastname | institution | authname | role | + | userA | Password1 | test01@example.com | Pete | Mc | mahara | internal | member | + Scenario: Change password + Given I log in as "userA" with password "Password1" And I follow "Settings" And I fill in "oldpassword" with "Password1" And I fill in "password1" with "Passwordnew" And I fill in "password2" with "Passwordnew" And I press "Save" + And I wait "1" seconds Then I should see "Preferences saved" Scenario: Change notifications - Given the following "users" exist: - | user | password | institution | role | - | userA | Password1 | mahara | member | - And I log in as "userA" with password "Password1" + Given I log in as "userA" with password "Password1" And I follow "Settings" And I follow "Notifications" And I select "Email" from "activity_viewaccess" And I press "Save" - Then I should see "Preferences saved" \ No newline at end of file + And I wait "1" seconds + Then I should see "Preferences saved" diff --git a/test/behat/features/artefact/blog/AddBlogs.feature b/test/behat/features/artefact/blog/AddBlogs.feature new file mode 100644 index 0000000000000000000000000000000000000000..a5ad366d1c9964a5fbcdf7271add4b2d99f8d215 --- /dev/null +++ b/test/behat/features/artefact/blog/AddBlogs.feature @@ -0,0 +1,31 @@ +@javascript @plugin @artefact.blog +Feature: Mahara users can create their blogs + As a mahara user + I need to create blogs + + Background: + Given the following "users" exist: + | username | password | email | firstname | lastname | institution | authname | role | + | userA | Password1 | test01@example.com | Pete | Mc | mahara | internal | member | + Scenario: create blogs + Given I log in as "userA" with password "Password1" + And I set the following account settings values: + | field | value | + | multipleblogs | 1 | + | tagssideblockmaxtags | 10 | + When I follow "Settings" + And I fill in the following: + | tagssideblockmaxtags | 10 | + And I check "multipleblogs" + And I press "Save" + And I wait "1" seconds + When I go to "artefact/blog/index.php" + And I wait until the page is ready + Then I should see "Journals" + When I click on "Create journal" + And I fill in the following: + | title | My new journal | + | tags | blog | +# And I set the field "description" to " This is my new blog " + And I press "Create journal" + Then I should see "My new journal" \ No newline at end of file diff --git a/test/behat/features/group/Group.feature b/test/behat/features/group/Group.feature new file mode 100644 index 0000000000000000000000000000000000000000..84f793640a3a60b1dc2b588fd0f9ba8cef37c64c --- /dev/null +++ b/test/behat/features/group/Group.feature @@ -0,0 +1,26 @@ +@javascript @core @group +Feature: Mahara users can participate in groups + As a mahara user + I need to participate in groups + + Background: + Given the following "users" exist: + | username | password | email | firstname | lastname | institution | authname | role | + | userA | Password1 | test01@example.com | Pete | Mc | mahara | internal | member | + | userB | Password1 | test02@example.com | Son | Nguyen | mahara | internal | member | + | userC | Password1 | test03@example.com | Jack | Smith | mahara | internal | member | + | userD | Password1 | test04@example.com | Eden | Wilson | mahara | internal | member | + | userE | Password1 | test05@example.com | Emily | Pham | mahara | internal | member | + And the following "groups" exist: + | name | owner | description | grouptype | open | invitefriends | editroles | submittableto | allowarchives | members | staff | + | group 01 | userA | This is group 01 | standard | ON | ON | all | ON | ON | userB, userC | userD | + Scenario: Join a group + Given I log in as "userE" with password "Password1" + When I go to "group/find.php" + And I wait "1" seconds + Then I should see "group 01" + When I click on "group 01" + Then I should see "About" + When I press "Join this group" + And I wait "1" seconds + Then I should see "You are now a group member." diff --git a/test/behat/features/institution/Institution.feature b/test/behat/features/institution/Institution.feature new file mode 100644 index 0000000000000000000000000000000000000000..7d3465a26d31eee6cb79024b022c09a51402e573 --- /dev/null +++ b/test/behat/features/institution/Institution.feature @@ -0,0 +1,21 @@ +@javascript @core @institution +Feature: Mahara users can be a member of an institution + As a mahara user + I can be a member of at least one institution + + Background: + Given the following "users" exist: + | username | password | email | firstname | lastname | institution | authname | role | + | userA | Password1 | test01@example.com | Pete | Mc | mahara | internal | member | + | userB | Password1 | test02@example.com | Son | Nguyen | mahara | internal | member | + | userC | Password1 | test03@example.com | Jack | Smith | mahara | internal | member | + | userD | Password1 | test04@example.com | Eden | Wilson | mahara | internal | member | + And the following "institutions" exist: + | name | displayname | registerallowed | registerconfirm | + | instone | Institution One | ON | OFF | + | insttwo | Institution Two | ON | OFF | + Scenario: Register to an institution + Given I log in as "userB" with password "Password1" + When I go to "account/institutions.php" + And I wait "1" seconds + Then I should see "Request membership of an institution" \ No newline at end of file diff --git a/test/behat/features/user/AddusersbyCSV.feature b/test/behat/features/user/AddusersbyCSV.feature new file mode 100644 index 0000000000000000000000000000000000000000..922e7a8265e1eed127d82d594ab7fe0c08a2d713 --- /dev/null +++ b/test/behat/features/user/AddusersbyCSV.feature @@ -0,0 +1,28 @@ +@javascript @core @admin @user +Feature: Mahara admins can add users via CSV files + As a site or institution admin + I can add users via CSV files + + Background: + Given the following "institutions" exist: + | name | displayname | registerallowed | registerconfirm | + | instone | Institution One | ON | OFF | + | insttwo | Institution Two | ON | OFF | + And the following "users" exist: + | username | password | email | firstname | lastname | institution | authname | role | + | userA | Password1 | test01@example.com | Pete | Mc | mahara | internal | admin | + | userB | Password1 | test02@example.com | Son | Nguyen | instone | internal | admin | + | userC | Password1 | test03@example.com | Jack | Smith | insttwo | internal | admin | + Scenario: As a site admin, add users via CSV file + Given I log in as "userA" with password "Password1" + When I go to "admin/users/uploadcsv.php" + And I wait "1" seconds + Then I should see "Add users by CSV" + When I attach the file "users.csv" to "uploadcsv_file" + And I wait "10" seconds + And I uncheck "uploadcsv_forcepasswordchange" + And I uncheck "uploadcsv_emailusers" + And I press "uploadcsv_submit" + And I wait "1" seconds + Then I should see "Your CSV file was processed successfully." + Then I should see "New users added: 5." \ No newline at end of file diff --git a/htdocs/user/tests/behat/Login.feature b/test/behat/features/user/Login.feature similarity index 100% rename from htdocs/user/tests/behat/Login.feature rename to test/behat/features/user/Login.feature diff --git a/test/behat/mahara_behat.sh b/test/behat/mahara_behat.sh new file mode 100755 index 0000000000000000000000000000000000000000..c53bf4f31935343daa4f42a617108f1a877a2382 --- /dev/null +++ b/test/behat/mahara_behat.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +function is_selenium_running { + res=$(curl -o /dev/null --silent --write-out '%{http_code}\n' http://localhost:4444/wd/hub/status) + if [[ $res == "200" ]]; then + return 0; + else + return 1; + fi +} + +# Check we are not running as root for some weird reason +if [[ "$USER" = "root" ]] +then + echo "This script should not be run as root" + exit 1 +fi + +# Get action and Mahara dir +ACTION=$1 +SCRIPTPATH=`readlink -f "${BASH_SOURCE[0]}"` +MAHARAROOT=`dirname $( dirname $( dirname "$SCRIPTPATH" ))` + +cd $MAHARAROOT + +if [ "$ACTION" = "action" ] +then + + # Wrap the util.php script + + PERFORM=$2 + php htdocs/testing/frameworks/behat/cli/util.php --$PERFORM + +elif [ "$ACTION" = "run" ] +then + + # Initialise the behat environment + php htdocs/testing/frameworks/behat/cli/init.php + + # Run the Behat tests themselves (after any intial setup) + TAGS=$2 + + if is_selenium_running; then + echo "Selenium is running" + else + echo "Start Selenium..." + + SELENIUM_VERSION_MAJOR=2.43 + SELENIUM_VERSION_MINOR=1 + + SELENIUM_FILENAME=selenium-server-standalone-$SELENIUM_VERSION_MAJOR.$SELENIUM_VERSION_MINOR.jar + SELENIUM_PATH=./test/behat/$SELENIUM_FILENAME + + # If no Selenium installed, download it + if [ ! -f $SELENIUM_PATH ]; then + echo "Downloading Selenium..." + wget -q -O $SELENIUM_PATH http://selenium-release.storage.googleapis.com/$SELENIUM_VERSION_MAJOR/$SELENIUM_FILENAME + echo "Downloaded" + fi + + java -jar $SELENIUM_PATH &>/dev/null & + sleep 5 + + if is_selenium_running; then + echo "Selenium started" + else + echo "Selenium can't be started" + exit 1 + fi + fi + + echo "Start PHP server" + php --server 127.0.0.1:8000 --docroot ./htdocs &>/dev/null & + SERVER=$! + + echo "Enable test site" + php htdocs/testing/frameworks/behat/cli/util.php --enable + + BEHATCONFIGFILE=`php htdocs/testing/frameworks/behat/cli/util.php --config` + echo "Run Behat..." + + if [ "$TAGS" ] + then + echo "Only run tests with the tag: $TAGS" + else + echo "Run all tests" + fi + + echo + echo "==================================================" + echo + + if [ "$TAGS" ] + then + ./external/vendor/bin/behat --config $BEHATCONFIGFILE --ansi --tags $TAGS + else + ./external/vendor/bin/behat --config $BEHATCONFIGFILE --ansi + fi + + echo + echo "==================================================" + echo + echo "Shutdown" + kill $SERVER +else + # Help text if we got an unexpected (or empty) first param + echo "Expected something like one of the following:" + echo + echo "# Run all tests:" + echo "mahara_behat run" + echo "" + echo "# Run tests with specific tag:" + echo "mahara_behat run @tagname" + echo "" + echo "# Enable test site:" + echo "mahara_behat action enable" + echo "" + echo "# Disable test site:" + echo "mahara_behat action disable" + echo "" + echo "# List other actions you can perform:" + echo "mahara_behat action help" + exit 1 +fi diff --git a/test/behat/stepdefinitions/BehatAccount.php b/test/behat/stepdefinitions/BehatAccount.php new file mode 100644 index 0000000000000000000000000000000000000000..67ab8c0bf411215c518a77bb395505e1dc1f05f1 --- /dev/null +++ b/test/behat/stepdefinitions/BehatAccount.php @@ -0,0 +1,94 @@ +getHash() as $accountpref) { + $prefs[$accountpref['field']] = $accountpref['value']; + } + + // Validate the settings + if (isset($prefs['urlid']) && get_config('cleanurls') && $prefs['urlid'] != $USER->get('urlid')) { + if (strlen($prefs['urlid']) < 3) { + throw new Exception("Invalid urlid: " . get_string('rule.minlength.minlength', 'pieforms', 3)); + } + else if (record_exists('usr', 'urlid', $prefs['urlid'])) { + throw new Exception("Invalid urlid: " . get_string('urlalreadytaken', 'account')); + } + } + + if (get_config('allowmobileuploads')) { + foreach ($prefs['mobileuploadtoken'] as $k => $text) { + if (strlen($text) > 0 && !preg_match('/^[a-zA-Z0-9 !@#$%^&*()\-_=+\[{\]};:\'",<\.>\/?]{6,}$/', $text)) { + throw new Exception("Invalid mobileuploadtoken: " . get_string('badmobileuploadtoken', 'account')); + } + } + } + + // Update user's account settings + db_begin(); + // use this as looping through values is not safe. + $expectedprefs = expected_account_preferences(); + if (isset($prefs['maildisabled']) && $prefs['maildisabled'] == 0 && get_account_preference($USER->get('id'), 'maildisabled') == 1) { + // Reset the sent and bounce counts otherwise mail will be disabled + // on the next send attempt + $u = new StdClass; + $u->email = $USER->get('email'); + $u->id = $USER->get('id'); + update_bounce_count($u,true); + update_send_count($u,true); + } + + // Remember the user's language & theme prefs, so we can reload the page if they change them + $oldlang = $USER->get_account_preference('lang'); + $oldtheme = $USER->get_account_preference('theme'); + $oldgroupsideblockmaxgroups = $USER->get_account_preference('groupsideblockmaxgroups'); + $oldgroupsideblocksortby = $USER->get_account_preference('groupsideblocksortby'); + + if (get_config('allowmobileuploads') && isset($prefs['mobileuploadtoken'])) { + // Make sure the mobile token is formatted / saved correctly + $prefs['mobileuploadtoken'] = array_filter($prefs['mobileuploadtoken']); + $new_token_pref = '|' . join('|', $prefs['mobileuploadtoken']) . '|'; + $USER->set_account_preference('mobileuploadtoken', $new_token_pref); + unset($prefs['mobileuploadtoken']); + } + + // Set user account preferences + foreach ($expectedprefs as $eprefkey => $epref) { + if (isset($prefs[$eprefkey]) && $prefs[$eprefkey] !== get_account_preference($USER->get('id'), $eprefkey)) { + $USER->set_account_preference($eprefkey, $prefs[$eprefkey]); + } + } + + db_commit(); + + } +} \ No newline at end of file diff --git a/test/behat/upload_files/users.csv b/test/behat/upload_files/users.csv new file mode 100644 index 0000000000000000000000000000000000000000..2b6b7729581c2f6708408306257e43a9db88a524 --- /dev/null +++ b/test/behat/upload_files/users.csv @@ -0,0 +1,6 @@ +username,password,email,firstname,lastname,studentid,preferredname +"u01","abc123","sonn+1@catalyst.net.nz","A","A","2010","Pete01" +"u02","abc123","sonn+2@catalyst.net.nz","B","B","2010","Toto02" +"u03","abc123","sonn+3@catalyst.net.nz","C","C","2010","Toto03" +"u04","abc123","sonn+4@catalyst.net.nz","D","D","2010", +"u10","abc123","sonn+10@catalyst.net.nz","K","K","2010","Kim"