'Rules Engine tests', 'description' => 'Test using the rules API to create and evaluate rules.', 'group' => 'Rules', ); } function setUp() { parent::setUp('rules', 'rules_test'); RulesLog::logger()->clear(); } /** * Calculates the output of t() given an array of placeholders to replace. */ static function t($text, $strings) { $placeholders = array(); foreach ($strings as $key => $string) { $key = !is_numeric($key) ? $key : $string; $placeholders['%' . $key] = drupal_placeholder($string); } return strtr($text, $placeholders); } protected function createTestRule() { $rule = rule(); $rule->condition('rules_test_condition_true') ->condition('rules_test_condition_true') ->condition(rules_or() ->condition(rules_condition('rules_test_condition_true')->negate()) ->condition('rules_test_condition_false') ->condition(rules_and() ->condition('rules_test_condition_false') ->condition('rules_test_condition_true') ->negate() ) ); $rule->action('rules_test_action'); return $rule; } /** * Tests creating a rule and iterating over the rule elements. */ function testRuleCreation() { $rule = $this->createTestRule(); $rule->integrityCheck(); $rule->execute(); $log = RulesLog::logger()->get(); $last = array_pop($log); $last = array_pop($log); $this->assertEqual($last[0], 'action called', 'Action called'); RulesLog::logger()->checkLog(); // Make sure condition and action iterators are working. $it = new RecursiveIteratorIterator($rule->conditions(), RecursiveIteratorIterator::SELF_FIRST); $this->assertEqual(iterator_count($it), 8, 'Iterated over all conditions and condition containers'); $it = new RecursiveIteratorIterator($rule->conditions()); $this->assertEqual(iterator_count($it), 6, 'Iterated over all conditions'); $this->assertEqual(iterator_count($rule->actions()), 1, 'Iterated over all actions'); // Test getting dependencies and the integrity check. $rule->integrityCheck(); $this->assertTrue($rule->dependencies() === array(), 'Dependencies correctly returned.'); } /** * Test getting dependencies. */ function testdependencies() { $action = rules_action('rules_node_publish_action'); $this->assertEqual($action->dependencies(), array('rules_test'), 'Providing module is returned as dependency.'); } /** * Test setting up an action with some action_info and serializing and * executing it. */ function testActionSetup() { $action = rules_action('rules_node_publish_action'); $s = serialize($action); $action2 = unserialize($s); $node = (object)array('status' => 0, 'type' => 'page'); $node->title = 'test'; $action2->execute($node); $this->assertEqual($node->status, 1, 'Action executed correctly'); $this->assertTrue(in_array('node', array_keys($action2->parameterInfo())), 'Parameter info returned.'); $node->status = 0; $action2->integrityCheck(); $action2->executeByArgs(array('node' => $node)); $this->assertEqual($node->status, 1, 'Action executed correctly'); // Test calling an extended + overriden method. $this->assertEqual($action2->help(), 'custom', 'Using custom help callback.'); // Inspect the cache //$this->pass(serialize(rules_get_cache())); RulesLog::logger()->checkLog(); } /** * Test executing with wrong arguments. */ function testActionExecutionFails() { $action = rules_action('rules_node_publish_action'); try { $action->execute(); $this->fail("Execution hasn't created an exception."); } catch (RulesException $e) { $this->pass("RulesException was thrown: ". $e); } } /** * Test setting up a rule and mapping variables. */ function testVariableMapping() { $rule = rule(array( 'node' => array('type' => 'node'), 'node_unchanged' => array('type' => 'node'), )); $rule->condition(rules_condition('rules_condition_content_is_published')->negate()) ->condition('rules_condition_content_is_type', array('type' => array('page', 'story'))) ->action('rules_node_publish_action', array('node:select' => 'node_unchanged')); $node1 = $this->drupalCreateNode(array('status' => 0, 'type' => 'page')); $node2 = $this->drupalCreateNode(array('status' => 0, 'type' => 'page')); $rule->integrityCheck(); $rule->execute($node1, $node2); $this->assertEqual($node2->status, 1, 'Action executed correctly on node2.'); $this->assertEqual($node1->status, 0, 'Action not executed on node1.'); RulesLog::logger()->checkLog(); } /** * Tests CRUD functionality. */ function testRulesCRUD() { $rule = $this->createTestRule(); $rule->integrityCheck()->save('test'); $this->assertEqual(TRUE, $rule->active, 'Rule is active.'); $this->assertEqual(0, $rule->weight, 'Rule weight is zero.'); $results = entity_load('rules_config', array('test')); $rule2 = array_pop($results); $this->assertEqual($rule->id, $rule2->id, 'Rule created and loaded'); $this->assertEqual(get_class($rule2), get_class($rule), 'Class properly instantiated.'); $rule2->execute(); // Update. $rule2->save(); // Make sure all rule elements are still here. $it = new RecursiveIteratorIterator($rule2->conditions(), RecursiveIteratorIterator::SELF_FIRST); $this->assertEqual(iterator_count($it), 8, 'Iterated over all conditions and condition containers'); $it = new RecursiveIteratorIterator($rule2->conditions()); $this->assertEqual(iterator_count($it), 6, 'Iterated over all conditions'); $this->assertEqual(iterator_count($rule2->actions()), 1, 'Iterated over all actions'); // Delete. $rule2->delete(); $this->assertEqual(entity_load('rules_config', FALSE, array('id' => $rule->id)), array(), 'Deleted.'); } /** * Test saving a parameter. */ function testActionSaving() { $action = rules_action('rules_node_publish_action_save'); $node = $this->drupalCreateNode(array('status' => 0, 'type' => 'page')); $action->executeByArgs(array('node' => $node)); $this->assertEqual($node->status, 1, 'Action executed correctly on node.'); // Sync node_load cache with node_save entity_get_controller('node')->resetCache(); $node = node_load($node->nid); $this->assertEqual($node->status, 1, 'Node has been saved.'); RulesLog::logger()->checkLog(); } /** * Test adding a variable and optional parameters. */ function testVariableAdding() { $node = $this->drupalCreateNode(); $rule = rule(array('nid' => array('type' => 'integer'))); $rule->condition('rules_test_condition_true') ->action('rules_action_load_node') ->action('rules_action_delete_node', array('node:select' => 'node_loaded')) ->execute($node->nid); $this->assertEqual(FALSE, node_load($node->nid), 'Variable added and skipped optional parameter.'); RulesLog::logger()->checkLog(); $vars = $rule->conditions()->offsetGet(0)->availableVariables(); $this->assertEqual(!isset($vars['node_loaded']), 'Loaded variable is not available to conditions.'); } /** * Test adding a variable with a custom variable name. */ function testVariableAddingCustom() { $node = $this->drupalCreateNode(); $rule = rule(array('nid' => array('type' => 'integer'))); $rule->action('rules_action_load_node', array('node_loaded:var' => 'node')) ->action('rules_action_delete_node') ->execute($node->nid); $this->assertEqual(FALSE, node_load($node->nid), 'Variable with custom name added.'); RulesLog::logger()->checkLog(); } /** * Test passing arguments by reference to an action. */ function testPassingByReference() { // Keeping references of variables is unsupported, though the // EntityMetadataArrayObject may be used to achieve that. $array = array('foo' => 'bar'); $data = new EntityMetadataArrayObject($array); rules_action('rules_action_test_reference')->execute($data); $this->assertTrue($data['changed'], 'Parameter has been passed by reference'); } /** * Test sorting rule elements. */ function testSorting() { $rule = $this->createTestRule(); $conditions = $rule->conditions(); $conditions[0]->weight = 10; $conditions[2]->weight = -1; // For testing use a deep sort, even if not necessary here. $rule->sortChildren(TRUE); $conditions = $rule->conditions(); $this->assertEqual($conditions[0]->weight, -1, 'Condition sorted correctly.'); $this->assertEqual($conditions[1]->weight, 0, 'Condition sorted correctly.'); $this->assertEqual($conditions[2]->weight, 10, 'Condition sorted correctly.'); } /** * Tests using data selectors. */ function testDataSelectors() { $body[LANGUAGE_NONE][0] = array('value' => 'The body & nothing.'); $node = $this->drupalCreateNode(array('body' => $body, 'type' => 'page', 'summary' => '')); $rule = rule(array('nid' => array('type' => 'integer'))); $rule->action('rules_action_load_node') ->action('drupal_message', array('message:select' => 'node_loaded:body:value')) ->execute($node->nid); RulesLog::logger()->checkLog(); $msg = drupal_get_messages(); $wrapper = entity_metadata_wrapper('node', $node); $this->assertEqual($msg['status'][0], $wrapper->body->value->value(array('sanitize' => TRUE)), 'Data selector for getting parameter applied.'); // Get a "reference" on the same object as returned by node_load(). $node = node_load($node->nid); $rule = rule(array('nid' => array('type' => 'integer'))); $rule->action('rules_action_load_node') ->action('data_set', array('data:select' => 'node_loaded:title', 'value' => 'Test title')) // Use two actions and make sure the node get saved only once. ->action('data_set', array('data:select' => 'node_loaded:title', 'value' => 'Test title2')) ->execute($node->nid); $wrapper = entity_metadata_wrapper('node', $node); $this->assertEqual('Test title2', $wrapper->title->value(), 'Data has been modified and saved.'); RulesLog::logger()->checkLog(); $text = RulesLog::logger()->render(); $msg = RulesTestCase::t('Saved %node_loaded of type %node.', array('node_loaded', 'node')); if ($pos1 = strpos($text, $msg)) { $pos2 = strpos($text, $msg, $pos1 + 1); } $this->assertTrue($pos1 && $pos2 === FALSE, 'Data has been saved only once.'); // Test validation. try { rules_action('data_set', array('data' => 'no-selector', 'value' => ''))->integrityCheck(); $this->fail("Validation hasn't created an exception."); } catch (RulesException $e) { $this->pass("Validation error correctly detected: ". $e); } } /** * Tests making use of rule sets. */ function testRuleSets() { $set = rules_rule_set(array( 'node' => array('type' => 'node', 'label' => 'node'), )); $set->rule(rule()->action('drupal_message', array('message:select' => 'node:title'))) ->rule(rule()->condition('rules_condition_content_is_published') ->action('drupal_message', array('message' => 'Node is published.')) ); $set->integrityCheck()->save('rules_test_set_1'); rules_clear_cache(TRUE); $node = $this->drupalCreateNode(array('title' => 'The title.', 'status' => 1)); // Execute. rules_invoke_component('rules_test_set_1', $node); $msg = drupal_get_messages(); $this->assertEqual($msg['status'][0], 'The title.', 'First rule evaluated.'); $this->assertEqual($msg['status'][1], 'Node is published.', 'Second rule evaluated.'); // Test a condition set. $set = rules_or(array( 'node' => array('type' => 'node', 'label' => 'node'), )); $set->condition('data_is', array('data:select' => 'node:author:name', 'value' => 'notthename')) ->condition('data_is', array('data:select' => 'node:nid', 'value' => $node->nid)) ->integrityCheck() ->save('test', 'rules_test'); // Load and execute condition set. $set = rules_config_load('test'); $this->assertTrue($set->execute($node), 'Set has been correctly evaluated.'); RulesLog::logger()->checkLog(); } /** * Tests invoking components from the action. */ function testComponentInvocations() { $set = rules_rule_set(array( 'node1' => array('type' => 'node', 'label' => 'node'), )); $set->rule(rule()->condition('node_is_published', array('node:select' => 'node1')) ->action('node_unpublish', array('node:select' => 'node1')) ); $set->integrityCheck()->save('rules_test_set_2'); rules_clear_cache(TRUE); // Use different names for the variables to ensure they are properly mapped // when taking over the variables to be saved. $rule = rule(array( 'node2' => array('type' => 'node', 'label' => 'node'), )); $rule->action('component_rules_test_set_2', array('node1:select' => 'node2')); $rule->action('node_make_sticky', array('node:select' => 'node2')); $node = $this->drupalCreateNode(array('title' => 'The title.', 'status' => 1, 'sticky' => 0)); $rule->execute($node); $node = node_load($node->nid, NULL, TRUE); $this->assertFalse($node->status, 'The component changes have been saved correctly.'); $this->assertTrue($node->sticky, 'The action changes have been saved correctly.'); // Check that we have saved the changes only once. $text = RulesLog::logger()->render(); // Make sure both saves are handled in one save operation. $this->assertEqual(substr_count($text, 'Saved'), 1, 'Changes have been saved in one save operation.'); RulesLog::logger()->checkLog(); // Test recursion prevention on components by invoking the component from // itself, what should be prevented. $set->action('component_rules_test_set_2', array('node1:select' => 'node1')) ->save(); rules_clear_cache(TRUE); $rule->execute($node); $text1 = RulesLog::logger()->render(); $text2 = RulesTestCase::t('Not evaluating rule set %rules_test_set_2 to prevent recursion.', array('rules_test_set_2')); $this->assertTrue((strpos($text1, $text2) !== FALSE), "Recursion of component invocation prevented."); } /** * Test asserting metadata, customizing action info and make sure integrity * is checked. */ function testMetadataAssertion() { $action = rules_action('rules_node_make_sticky_action'); // Test failing integrity check. try { $rule = rule(array('node' => array('type' => 'entity'))); $rule->action($action); // Fails due to the 'node' variable not matching the node type. $rule->integrityCheck(); $this->fail('Integrity check has not thrown an exception.'); } catch (RulesException $e) { $this->pass('Integrity check has thrown exception: ' . $e->getMessage()); } // Test asserting additional metadata. $rule = rule(array('node' => array('type' => 'node'))); // Customize action info using the settings. $rule->condition('data_is', array('data:select' => 'node:type', 'value' => 'page')) // Configure an condition using the body. As the body is a field, // tis requires the bundle to be correctly asserted. ->condition(rules_condition('data_is', array('data:select' => 'node:body:value', 'value' => 'foo'))->negate()) // The action also requires the page bundle in order to work. ->action($action); // Make sure the integrity check doesn't throw an exception. $rule->integrityCheck(); // Test the rule. $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0)); $rule->execute($node); $this->assertTrue($node->sticky, 'Rule with asserted metadata executed.'); RulesLog::logger()->checkLog(); } /** * Test using loops. */ function testLoops() { // Test passing the list parameter as argument to ensure that is working // generally for plugin container too. $loop = rules_loop(); $loop->action('drupal_message', array('message' => 'test')); $arg_info = $loop->parameterInfo(); $this->assert($arg_info['list']['type'] == 'list', 'Argument info contains list.'); $loop->execute(array(1, 2)); // Ensure the action has been executed twice, once for each list item. $msg = drupal_get_messages(); $this->assert($msg['status'][0] == 'test' && $msg['status'][1], 'Loop has been properly executed'); // Now test looping over nodes. $node1 = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0)); $node2 = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0)); $node3 = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0)); $rule = rule(array( 'list' => array( 'type' => 'list', 'label' => 'A list of nodes', ) )); $loop = rules_loop(array('list:select' => 'list', 'item:var' => 'node')); $loop->action('data_set', array('data:select' => 'node:sticky', 'value' => TRUE)); $rule->action($loop); // Test using a list with data selectors, just output the last nodes type. $rule->action('drupal_message', array('message:select' => 'list:2:type')); $rule->execute(array($node1->nid, $node2->nid, $node3->nid)); $text = RulesLog::logger()->render(); $save_msg = RulesTestCase::t('Saved %node of type %node.', array('node', 'node')); $this->assertTrue(substr_count($text, $save_msg) == 3, 'List item variables have been saved.'); RulesLog::logger()->checkLog(); } /** * Test access checks. */ function testAccessCheck() { $rule = rule(); // Try to set a property which is provided by the test module and is not // accessible, so the access check has to return FALSE. $rule->action('data_set', array('data:select' => 'site:no-access-user', 'value' => 'foo')); $this->assertTrue($rule->access() === FALSE, 'Access check is working.'); } /** * Test returning provided variables. */ function testReturningVariables() { $node = $this->drupalCreateNode(); $action = rules_action('entity_fetch', array('type' => 'node', 'id' => $node->nid)); list($node2) = $action->execute(); $this->assertTrue($node2->nid == $node->nid, 'Action returned a variable.'); // Create a simple set that just passed through the given node. $set = rules_rule_set(array('node' => array('type' => 'node')), array('node')); $set->integrityCheck()->save('rules_test_set_1'); $provides = $set->providesVariables(); $this->assertTrue($provides['node']['type'] == 'node', 'Rule set correctly passed through the node.'); list($node2) = $set->execute($node); $this->assertTrue($node2->nid == $node->nid, 'Rule set returned a variable.'); } /** * Tests using input evaluators. */ function testInputEvaluators() { $node = $this->drupalCreateNode(array('title' => 'The body & nothing.', 'type' => 'page')); $rule = rule(array('nid' => array('type' => 'integer'))); $rule->action('rules_action_load_node') ->action('drupal_message', array('message' => 'Title: [node_loaded:title]')) ->execute($node->nid); RulesLog::logger()->checkLog(); $msg = drupal_get_messages(); $this->assertEqual(array_pop($msg['status']), 'Title: ' . check_plain('The body & nothing.'), 'Token input evaluator applied.'); } /** * Test exporting a rule. */ function testRuleExport() { $rule = rule(array('nid' => array('type' => 'integer'))); $rule->name = "rules_export_test"; $rule->action('rules_action_load_node') ->action('drupal_message', array('message' => 'Title: [node_loaded:title]')); $export = '{ "rules_export_test" : { "PLUGIN" : "rule", "REQUIRES" : [ "rules_test", "rules" ], "USES VARIABLES" : { "nid" : { "type" : "integer" } }, "DO" : [ { "rules_action_load_node" : { "PROVIDE" : { "node_loaded" : { "node_loaded" : "Loaded content" } } } }, { "drupal_message" : { "message" : "Title: [node_loaded:title]" } } ] } }'; $this->assertEqual($export, $rule->export(), 'Rule has been exported correctly.'); } /** * Test the named parameter mode. */ function testNamedParameters() { $rule = rule(array('node' => array('type' => 'node'))); $rule->action('rules_action_node_set_title', array('title' => 'foo')); $rule->integrityCheck(); // Test the rule. $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0)); $rule->execute($node); $this->assertTrue($node->title == 'foo', 'Action with named parameters has been correctly executed.'); RulesLog::logger()->checkLog(); } function testRuleBuilding() { //TODO: implement. return; $configs = rules_config_build( '{ "rules_tzhxvirp" : { "LABEL" : "example default rule", "PLUGIN" : "reaction rule", "REQUIRES" : [ "rules" ], "ON" : [ "node_update" ], "IF" : [ { "data_is" : { "data" : [ "node:status" ], "value" : true } }, { "data_is" : { "data" : [ "node:type" ], "value" : "page" } } ], "DO" : [ { "drupal_message" : { "message" : "A node has been updated." } } ] } }' )->execute(); } } function rules_test_condition_true($settings, $state, $element) { if (!$element instanceof RulesCondition) { throw new Exception('Rules element has not been passed to condition.'); } rules_log('condition true called'); return TRUE; } function rules_test_condition_false() { rules_log('condition false called'); return FALSE; } function rules_test_action() { rules_log('action called'); } /** * Test rules data wrappers. */ class RulesTestDataCase extends DrupalWebTestCase { static function getInfo() { return array( 'name' => 'Rules Data tests', 'description' => 'Tests rules data saving and type matching.', 'group' => 'Rules', ); } function setUp() { parent::setUp('rules', 'rules_test'); // Make sure we don't ran over issues with the node_load static cache. entity_get_controller('node')->resetCache(); } /** * Tests intelligently saving data. */ function testDataSaving() { $node = $this->drupalCreateNode(); $state = new RulesState(rule()); $state->addVariable('node', $node, array('type' => 'node')); $wrapper = $state->get('node'); $node->title = 'test'; $wrapper->set($node); $state->saveChanges('node', $wrapper, FALSE); $this->assertFalse($this->drupalGetNodeByTitle('test'), 'Changes have not been saved.'); $state->saveChanges('node', $wrapper, TRUE); $this->assertTrue($this->drupalGetNodeByTitle('test'), 'Changes have been saved.'); // Test skipping saving. $state->addVariable('node2', $node, array( 'type' => 'node', 'skip save' => TRUE, )); $wrapper = $state->get('node2'); $node->title = 'test2'; $wrapper->set($node); $state->saveChanges('node2', $wrapper, TRUE); $this->assertFalse($this->drupalGetNodeByTitle('test2'), 'Changes have not been saved.'); // Try saving a non-entity wrapper, which should result in saving the // parent entity containing the property. $wrapper = $state->get('node'); $wrapper->title->set('test3'); $state->saveChanges('node:title', $wrapper, TRUE); $this->assertTrue($this->drupalGetNodeByTitle('test3'), 'Parent entity has been saved.'); } /** * Test type matching */ function testTypeMatching() { $entity = array('type' => 'entity'); $node = array('type' => 'node'); $this->assertTrue(RulesData::typesMatch($node, $entity), 'Types match.'); $this->assertFalse(RulesData::typesMatch($entity, $node), 'Types don\'t match.'); $this->assertTrue(RulesData::typesMatch($node + array('bundle' => 'page'), $node), 'Types match.'); $this->assertTrue(RulesData::typesMatch($node + array('bundle' => 'page'), $entity), 'Types match.'); $this->assertTrue(RulesData::typesMatch(array('type' => 'list'), array('type' => 'list')), 'Types match.'); $this->assertTrue(RulesData::typesMatch($node + array('bundle' => 'page'), $node + array('bundles' => array('page', 'story'))), 'Types match.'); $this->assertFalse(RulesData::typesMatch($node, $node + array('bundles' => array('page', 'story'))), 'Types don\'t match.'); // Test that a type matches its grand-parent type (text > decimal > integer) $this->assertTrue(RulesData::typesMatch(array('type' => 'integer'), array('type' => 'text')), 'Types match.'); $this->assertFalse(RulesData::typesMatch(array('type' => 'text'), array('type' => 'integer')), 'Types don\'t match.'); } } /** * Test triggering rules. */ class RulesTriggerTestCase extends DrupalWebTestCase { static function getInfo() { return array( 'name' => 'Reaction Rules', 'description' => 'Tests triggering reactive rules.', 'group' => 'Rules', ); } function setUp() { parent::setUp('rules', 'rules_test'); RulesLog::logger()->clear(); } protected function createTestRule($action = TRUE, $event = 'node_presave') { $rule = rules_reaction_rule(); $rule->event($event) ->condition(rules_condition('data_is', array('data:select' => 'node:status', 'value' => TRUE))->negate()) ->condition('data_is', array('data:select' => 'node:type', 'value' => 'page')); if ($action) { $rule->action('rules_action_delete_node'); } return $rule; } /** * Tests CRUD for reaction rules - making sure the events are stored properly. */ function testReactiveRuleCreation() { $rule = $this->createTestRule(); $rule->save(); $result = db_query("SELECT event FROM {rules_trigger} WHERE id = :id", array(':id' => $rule->id)); $this->assertEqual($result->fetchField(), 'node_presave', 'Associated event has been saved.'); // Try updating. $events =& $rule->events(); unset($events[0]); $events[] = 'node_insert'; $events[] = 'node_update'; $rule->active = FALSE; $rule->integrityCheck()->save(); $result = db_query("SELECT event FROM {rules_trigger} WHERE id = :id", array(':id' => $rule->id)); $this->assertEqual($result->fetchCol(), array_values($events), 'Updated associated events.'); // Try deleting. $rule->delete(); $result = db_query("SELECT event FROM {rules_trigger} WHERE id = :id", array(':id' => $rule->id)); $this->assertEqual($result->fetchField(), FALSE, 'Deleted associated events.'); } /** * Tests creating and triggering a basic reaction rule. */ function testBasicReactionRule() { $node = $this->drupalCreateNode(array('type' => 'page')); $rule = $this->createTestRule(); $rule->integrityCheck()->save(); // Force immediate cache clearing so we can test the rule *now*. rules_clear_cache(TRUE); // Test the basics of the event set work right. $event = rules_get_cache('event_node_presave'); $this->assertEqual(array_keys($event->parameterInfo()), array('node'), 'EventSet returns correct argument info.'); // Trigger the rule by updating the node. $nid = $node->nid; $node->status = 0; node_save($node); RulesLog::logger()->checkLog(); $this->assertFalse(node_load($nid), 'Rule successfully triggered and executed'); //debug(RulesLog::logger()->render()); } /** * Test a rule using a handler to load a variable. */ function testVariableHandler() { $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); $rule = $this->createTestRule(FALSE, 'node_update'); $rule->action('rules_node_publish_action_save', array('node:select' => 'node_unchanged')); // Test without recursion prevention to make sure recursive invocations // work right too. This rule won't ran in an infinite loop anyway. $rule->recursion = TRUE; $rule->label = 'rule 1'; $rule->integrityCheck()->save(); rules_clear_cache(TRUE); $node->status = 0; $node->sticky = 1; node_save($node); RulesLog::logger()->checkLog(); entity_get_controller('node')->resetCache(); $node = node_load($node->nid); $this->assertFalse($node->sticky, 'Parameter has been loaded and saved.'); $this->assertTrue($node->status, 'Action has been executed.'); // Ensure the rule was evaluated a second time $text = RulesLog::logger()->render(); $msg = RulesTestCase::t('Evaluating rule %rule 1', array('rule 1')); $pos = strpos($text, $msg); $pos = ($pos !== FALSE) ? strpos($text, $msg, $pos) : FALSE; $this->assertTrue($pos !== FALSE, "Recursion prevented."); //debug(RulesLog::logger()->render()); } /** * Test aborting silently when handlers are not able to load. */ function testVariableHandlerFailing() { $rule = $this->createTestRule(FALSE, 'node_presave'); $rule->action('rules_node_publish_action_save', array('node:select' => 'node_unchanged')); $rule->integrityCheck()->save(); rules_clear_cache(TRUE); // On insert it's not possible to get the unchanged node during presave. $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); //debug(RulesLog::logger()->render()); $text = RulesTestCase::t('Unable to load variable %node_unchanged, aborting.', array('node_unchanged')); $this->assertTrue(strpos(RulesLog::logger()->render(), $text) !== FALSE, "Aborted evaluation."); } /** * Tests preventing recursive rule invocations by creating a rule that reacts * on node-update and generates a node update that would trigger it itself. */ function testRecursionPrevention() { $rule = $this->createTestRule(FALSE, 'node_update'); $rule->action('rules_node_make_sticky_action'); $rule->integrityCheck()->save(); rules_clear_cache(TRUE); // Now trigger the rule. $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); node_save($node); $text = RulesTestCase::t('Not evaluating reaction rule %label to prevent recursion.', array('label' => $rule->name)); //debug(RulesLog::logger()->render()); $this->assertTrue((strpos(RulesLog::logger()->render(), $text) !== FALSE), "Recursion prevented."); //debug(RulesLog::logger()->render()); } /** * Ensure the recursion prevention still allows to let the rule trigger again * during evaluation of the same event set, if the event isn't caused by the * rule itself - thus we won't run in an infinte loop. */ function testRecursionOnDifferentArguments() { // Create rule1 - which might recurse. $rule = $this->createTestRule(FALSE, 'node_update'); $rule->action('rules_node_make_sticky_action'); $rule->label = 'rule 1'; $rule->integrityCheck()->save(); // Create rule2 - which triggers rule1 on another node. $node2 = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); $rule2 = $this->createTestRule(FALSE, 'node_update'); $rule2->action('rules_action_load_node', array('nid' => $node2->nid)) ->action('rules_node_make_sticky_action', array('node:select' => 'node_loaded')); $rule2->label = 'rule 2'; $rule2->save(); rules_clear_cache(TRUE); // Now trigger both rules by generating the event. $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); node_save($node); //debug(RulesLog::logger()->render()); $text = RulesLog::logger()->render(); $pos = strpos($text, RulesTestCase::t('Evaluating rule %rule 1', array('rule 1'))); $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Evaluating rule %rule 2', array('rule 2')), $pos) : FALSE; $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Saved %node_loaded of type %node.', array('node_loaded', 'node')), $pos) : FALSE; $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Evaluating rule %rule 1', array('rule 1')), $pos) : FALSE; $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Not evaluating reaction rule %rule 2 to prevent recursion', array('rule 2')), $pos) : FALSE; $this->assertTrue($pos !== FALSE, 'Rule1 was triggered on the event caused by Rule2.'); } /** * Tests the provided default rule 'rules_test_default_1'. */ function testDefaultRule() { $rule = rules_config_load('rules_test_default_1'); $this->assertTrue($rule->status & ENTITY_IN_CODE && !($rule->status & ENTITY_IN_DB), 'Default rule can be loaded and has the right status.'); // Enable. $rule->active = TRUE; $rule->save(); rules_clear_cache(TRUE); // Create a node that triggers the rule. $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); // Clear messages. drupal_get_messages(); // Let event node_update occur. node_save($node); $msg = drupal_get_messages(); $this->assertEqual($msg['status'][0], 'A node has been updated.', 'Default rule has been triggered.'); } /** * Tests the drupal goto action. */ function testRedirectAction() { $rule = rules_reaction_rule(); $rule->event('user_login') ->action('redirect', array('url' => 'user/[account:uid]/edit')) ->save(); rules_clear_cache(TRUE); $user = $this->drupalCreateUser(); $this->drupalLogin($user); // Make sure the right URL has been generated and we are there now. // The action currently does not work correctly. // @todo: Reenable once it has been fixed. //$this->assertTrue(strpos($this->getUrl(), "user/$user->uid/edit") !== FALSE, 'Redirected to the right url.'); } } /** * Tests provided module integration. */ class RulesIntegrationTestCase extends DrupalWebTestCase { static function getInfo() { return array( 'name' => 'Rules Core Integration', 'description' => 'Tests provided integration for drupal core.', 'group' => 'Rules', ); } function setUp() { parent::setUp('rules', 'rules_test', 'php', 'path'); RulesLog::logger()->clear(); } function testCRUDActions() { // Test creation. $action = rules_action('entity_create', array( 'type' => 'node', 'param_type' => 'page', 'param_title' => 'foo', 'param_author' => $GLOBALS['user'], )); $action->access(); $action->execute(); $text = RulesLog::logger()->render(); $pos = strpos($text, RulesTestCase::t('Added the provided variable %entity_created of type %node', array('entity_created', 'node'))); $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Saved %entity_created of type %node.', array('entity_created', 'node')), $pos) : FALSE; $this->assertTrue($pos !== FALSE, 'Data has been created and saved.'); $node = $this->drupalCreateNode(array('type' => 'page', 'sticky' => 0, 'status' => 0)); $rule = rule(); $rule->action('entity_fetch', array('type' => 'node', 'id' => $node->nid, 'entity_fetched:var' => 'node')); $rule->action('entity_save', array('data:select' => 'node', 'immediate' => TRUE)); $rule->action('entity_delete', array('data:select' => 'node')); $rule->access(); $rule->integrityCheck()->execute(); $text = RulesLog::logger()->render(); $pos = strpos($text, RulesTestCase::t('Evaluating the action %entity_fetch.', array('entity_fetch'))); $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Added the provided variable %node of type %node', array('node')), $pos) : FALSE; $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Saved %node of type %node.', array('node')), $pos) : FALSE; $pos = ($pos !== FALSE) ? strpos($text, RulesTestCase::t('Evaluating the action %entity_delete.', array('entity_delete')), $pos) : FALSE; $this->assertTrue($pos !== FALSE, 'Data has been fetched, saved and deleted.'); //debug(RulesLog::logger()->render()); } /** * Test the data create action. */ function testCreateData() { $action = rules_action('data_create', array( 'type' => 'log_entry', 'param_type' => 'rules_test', 'param_message' => 'Rules test log message', 'param_severity' => WATCHDOG_WARNING, 'param_request_uri' => 'http://example.com', 'param_link' => '', )); $action->access(); $action->execute(); $text = RulesLog::logger()->render(); $pos = strpos($text, RulesTestCase::t('Added the provided variable %data_created of type %log_entry', array('data_created', 'log_entry'))); $this->assertTrue($pos !== FALSE, 'Data of type log entry has been created.'); } /** * Test the variable add action. */ function testVariableAdd() { // Test creation. $action = rules_action('variable_add', array( 'type' => 'text_formatted', 'value' => array( 'value' => 'test text', 'format' => 1, ) )); $action->access(); $action->execute(); $text = RulesLog::logger()->render(); $pos = strpos($text, RulesTestCase::t('Added the provided variable %variable_added of type %text_formatted', array('variable_added', 'text_formatted'))); $this->assertTrue($pos !== FALSE, 'Data of type text formatted has been created.'); } function testDataQueryAction() { $node = $this->drupalCreateNode(array('type' => 'page', 'title' => 'foo')); $rule = rule(); $rule->action('entity_query', array('type' => 'node', 'property' => 'title', 'value' => 'foo')) ->action('data_set', array('data:select' => 'entity_fetched:0:title', 'value' => 'bar')); $rule->access(); $rule->integrityCheck(); $rule->execute(); RulesLog::logger()->checkLog(); //debug(RulesLog::logger()->render()); $node = node_load($node->nid); $this->assertEqual('bar', $node->title, 'Fetched a node by title and modified it.'); } /** * Just make sure the access callback run without errors. */ function testAccessCallbacks() { $cache = rules_get_cache(); foreach (array('action', 'condition', 'event') as $type) { foreach (rules_fetch_data($type . '_info') as $name => $info) { if (isset($info['access callback'])) { $info['access callback']($type, $name); } } } } /** * Test reacting on new log entries and make sure the log entry is usable. */ function testWatchdog() { $rule = rules_reaction_rule(); $rule->event('watchdog'); $rule->action('drupal_message', array('message:select' => 'log_entry:message')); $rule->integrityCheck()->save('test_watchdog'); rules_clear_cache(TRUE); watchdog('php', 'test %message', array('%message' => 'message')); $msg = drupal_get_messages(); $this->assertEqual(array_pop($msg['status']), t('test %message', array('%message' => 'message')), 'Watchdog event occured and log entry properties can be used.'); } /** * Test the provided list actions. */ function testListActions() { $rule = rule(array( 'list' => array( 'type' => 'list', 'label' => 'A list of text', ) )); $rule->action('list_add', array('list:select' => 'list', 'item' => 'bar2')); $rule->action('list_add', array('list:select' => 'list', 'item' => 'bar', 'pos' => 'start')); $rule->action('list_remove', array('list:select' => 'list', 'item' => 'bar2')); $list = entity_metadata_wrapper('list', array('foo', 'foo2')); $rule->execute($list); RulesLog::logger()->checkLog(); $this->assertEqual($list->value(), array('bar', 'foo', 'foo2'), 'List items removed and added.'); //debug(RulesLog::logger()->render()); } /** * Tests entity related integration. */ function testEntityIntegration() { global $user; $node = entity_property_values_create_entity('node', array( 'type' => 'article', 'author' => $user, 'title' => 'foo', ))->value(); $term_wrapped = entity_property_values_create_entity('taxonomy_term', array( 'name' => $this->randomName(), 'vocabulary' => 1, ))->save(); // Test asserting the field and using it afterwards. $rule = rule(array('node' => array('type' => 'node'))); $rule->condition('entity_has_field', array('entity:select' => 'node', 'field' => 'field_tags')); $rule->condition('entity_is_new', array('entity:select' => 'node')); $rule->action('list_add', array('list:select' => 'node:field-tags', 'item' => $term_wrapped)); $rule->integrityCheck(); $rule->execute($node); $tid = $term_wrapped->getIdentifier(); RulesLog::logger()->checkLog(); $this->assertEqual(array_values($node->field_tags[LANGUAGE_NONE]), array(0 => array('tid' => $tid)), 'Entity has field conditions evaluted.'); // Test loading a non-node entity. $action = rules_action('entity_fetch', array('type' => 'taxonomy_term', 'id' => $tid)); list($term) = $action->execute(); $this->assertEqual($term->tid, $tid, 'Fetched a taxonomy term using "entity_fetch".'); } /** * Test integration for the taxonomy module. */ function testTaxonomyIntegration() { $term = entity_property_values_create_entity('taxonomy_term', array( 'name' => $this->randomName(), 'vocabulary' => 1, ))->value(); $term2 = clone $term; taxonomy_term_save($term); taxonomy_term_save($term2); $tags[LANGUAGE_NONE][0]['tid'] = $term->tid; $node = $this->drupalCreateNode(array('title' => 'foo', 'type' => 'article', 'field_tags' => $tags)); // Test assigning and remove a term from an article. $rule = rule(array('node' => array('type' => 'node', 'bundle' => 'article'))); $term_wrapped = rules_wrap_data($term->tid, array('type' => 'taxonomy_term')); $term_wrapped2 = rules_wrap_data($term2->tid, array('type' => 'taxonomy_term')); $rule->action('list_add', array('list:select' => 'node:field-tags', 'item' => $term_wrapped2)); $rule->action('list_remove', array('list:select' => 'node:field-tags', 'item' => $term_wrapped)); $rule->execute($node); RulesLog::logger()->checkLog(); $this->assertEqual(array_values($node->field_tags[LANGUAGE_NONE]), array(0 => array('tid' => $term2->tid)), 'Term removed and added from a node.'); // Test using the taxonomy term reference field on a term object. $field_name = drupal_strtolower($this->randomName() . '_field_name'); $field = field_create_field(array( 'field_name' => $field_name, 'type' => 'taxonomy_term_reference', // Set cardinality to unlimited for tagging. 'cardinality' => FIELD_CARDINALITY_UNLIMITED, 'settings' => array( 'allowed_values' => array( array( 'vocabulary' => 'tags', 'parent' => 0, ), ), ), )); $instance = array( 'field_name' => $field_name, 'entity_type' => 'taxonomy_term', 'bundle' => 'tags', // Machine name of vocabulary. 'label' => $this->randomName() . '_label', 'description' => $this->randomName() . '_description', 'weight' => mt_rand(0, 127), 'widget' => array( 'type' => 'taxonomy_autocomplete', 'weight' => -4, ), 'display' => array( 'default' => array( 'type' => 'taxonomy_term_reference_link', 'weight' => 10, ), ), ); field_create_instance($instance); $term1 = entity_property_values_create_entity('taxonomy_term', array( 'name' => $this->randomName(), 'vocabulary' => 1, ))->save(); $term2 = entity_property_values_create_entity('taxonomy_term', array( 'name' => $this->randomName(), 'vocabulary' => 1, ))->save(); // Test asserting the term reference field and using it afterwards. $rule = rule(array('taxonomy_term' => array('type' => 'taxonomy_term'))); $rule->condition('entity_has_field', array('entity:select' => 'taxonomy-term', 'field' => $field_name)); // Add $term2 to $term1 using the term reference field. $selector = str_replace('_', '-', 'taxonomy_term:' . $field_name); $rule->action('list_add', array('list:select' => $selector, 'item' => $term2)); $rule->integrityCheck(); $rule->execute($term1); RulesLog::logger()->checkLog(); $this->assertEqual($term1->{$field_name}[0]->getIdentifier(), $term2->getIdentifier(), 'Rule appended a term to the term reference field on a term.'); } /** * Test integration for the node module. */ function testNodeIntegration() { $tests = array( array('node_unpublish', 'node_is_published', 'node_publish', 'status'), array('node_make_unsticky', 'node_is_sticky', 'node_make_sticky', 'sticky'), array('node_unpromote', 'node_is_promoted', 'node_promote', 'promote'), ); $node = $this->drupalCreateNode(array('type' => 'page', 'status' => 1, 'sticky' => 1, 'promote' => 1)); foreach ($tests as $info) { list($action1, $condition, $action2, $property) = $info; rules_action($action1)->execute($node); $node = node_load($node->nid, NULL, TRUE); $this->assertFalse($node->$property, 'Action has permanently disabled node '. $property); $return = rules_condition($condition)->execute($node); $this->assertFalse($return, 'Condition determines node '. $property . ' is disabled.'); rules_action($action2)->execute($node); $node = node_load($node->nid, NULL, TRUE); $this->assertTrue($node->$property, 'Action has permanently enabled node '. $property); $return = rules_condition($condition)->execute($node); $this->assertTrue($return, 'Condition determines node '. $property . ' is enabled.'); } $return = rules_condition('node_is_of_type', array('type' => array('page', 'article')))->execute($node); $this->assertTrue($return, 'Condition determines node is of type page.'); $return = rules_condition('node_is_of_type', array('type' => array('article')))->execute($node); $this->assertFalse($return, 'Condition determines node is not of type article.'); RulesLog::logger()->checkLog(); } /** * Test integration for the user module. */ function testUserIntegration() { $rid = $this->drupalCreateRole(array('administer nodes'), 'foo'); $user = $this->drupalCreateUser(); // Test assigning a role with the list_add action. $rule = rule(array('user' => array('type' => 'user'))); $rule->action('list_add', array('list:select' => 'user:roles', 'item' => $rid)); $rule->execute($user); $this->assertTrue(isset($user->roles[$rid]), 'Role assigned to user.'); // Test removing a role with the list_remove action. $rule = rule(array('user' => array('type' => 'user'))); $rule->action('list_remove', array('list:select' => 'user:roles', 'item' => $rid)); $rule->execute($user); $this->assertTrue(!isset($user->roles[$rid]), 'Role removed from user.'); // Test assigning a role with user_add_role action. $rule = rule(array('user' => array('type' => 'user'))); $rule->action('user_add_role', array('account:select' => 'user', 'roles' => array($rid))); $rule->execute($user); $user = user_load($user->uid, TRUE); $result = rules_condition('user_has_role', array('roles' => array($rid)))->execute($user); $this->assertTrue($result, 'Role assigned to user.'); // Test removing a role with the user_remove_role action. $rule = rule(array('user' => array('type' => 'user'))); $rule->action('user_remove_role', array('account:select' => 'user', 'roles' => array($rid))); $rule->execute($user); $user = user_load($user->uid, TRUE); $result = rules_condition('user_has_role', array('roles' => array($rid)))->execute($user); $this->assertFalse($result, 'Role removed from user.'); // Test user blocking. rules_action('user_block')->execute($user); $user = user_load($user->uid, TRUE); $this->assertTrue(rules_condition('user_is_blocked')->execute($user), 'User has been blocked.'); rules_action('user_unblock')->execute($user); $user = user_load($user->uid, TRUE); $this->assertFalse(rules_condition('user_is_blocked')->execute($user), 'User has been unblocked.'); RulesLog::logger()->checkLog(); } /** * Test integration for the php module. */ function testPHPIntegration() { $node = $this->drupalCreateNode(array('title' => 'foo')); $rule = rule(array('var_name' => array('type' => 'node'))); $rule->condition('php_eval', array('code' => 'return TRUE;')) ->action('php_eval', array('code' => 'drupal_set_message("Executed-" . $var_name->title);')) ->action('drupal_message', array('message' => 'Title: title; ?> Token: [var_name:title]')); $rule->execute($node); $rule->access(); RulesLog::logger()->checkLog(); $msg = drupal_get_messages(); $this->assertEqual(array_pop($msg['status']), "Title: foo Token: foo", 'PHP input evaluation has been applied.'); $this->assertEqual(array_pop($msg['status']), "Executed-foo", 'PHP code condition and action have been evaluated.'); // Test PHP data processor $rule = rule(array('var_name' => array('type' => 'node'))); $rule->action('drupal_message', array( 'message:select' => 'var_name:title', 'message:process' => array( 'php' => array('code' => 'return "Title: $value";') ), )); $rule->execute($node); $rule->access(); RulesLog::logger()->checkLog(); $msg = drupal_get_messages(); $this->assertEqual(array_pop($msg['status']), "Title: foo", 'PHP data processor has been applied.'); } /** * Makes sure the date input evaluator evaluates properly using strtotime(). */ function testDateInputEvaluator() { $node = $this->drupalCreateNode(array('title' => 'foo')); $rule = rule(array('node' => array('type' => 'node'))); $rule->action('data_set', array('data:select' => 'node:created', 'value' => '+1 day')); $rule->execute($node); RulesLog::logger()->checkLog(); $node = node_load($node->nid, NULL, TRUE); $now = RulesDateInputEvaluator::gmstrtotime('now'); // Tolerate a difference of a second. $this->assertTrue(abs($node->created - $now - 86400) <= 1, 'Date input has been evaluated.'); } /** * Test using a date offset. */ function testDateOffsetProcessor() { $node = $this->drupalCreateNode(array('title' => 'foo')); $rule = rule(array('node' => array('type' => 'node'))); $rule->action('data_set', array( 'data:select' => 'node:created', 'value:select' => 'node:created', 'value:process' => array( 'date_offset' => array('value' => 86400), ), )); $created_orig = $node->created; $rule->execute($node); RulesLog::logger()->checkLog(); $node = node_load($node->nid, NULL, TRUE); $this->assertTrue(($node->created - $created_orig - 86400) == 0, 'Date offset has been added.'); } /** * Test site/system integration. */ function testSystemIntegration() { // Test using the 'site' variable. $condition = rules_condition('data_is', array('data:select' => 'site:current-user:name', 'value' => $GLOBALS['user']->name)); $this->assertTrue($condition->execute(), 'Retrieved the current user\'s name.'); // Another test using a token replacement. $condition = rules_condition('data_is', array('data:select' => 'site:current-user:name', 'value' => '[site:current-user:name]')); $this->assertTrue($condition->execute(), 'Replaced the token for the current user\'s name.'); // Test breadcrumbs and drupal set message. $rule = rules_reaction_rule(); $rule->event('init') ->action('breadcrumb_set', array('titles' => array('foo'), 'paths' => array('bar'))) ->action('drupal_message', array('message' => 'A message.')); $rule->save('test'); rules_clear_cache(); $this->drupalGet('node'); $this->assertLink('foo', 0, 'Breadcrumb has been set.'); $this->assertText('A message.', 'Drupal message has been shown.'); // Test the page redirect. $node = $this->drupalCreateNode(); $rule = rules_reaction_rule(); $rule->event('node_view') ->action('redirect', array('url' => 'user')); $rule->save('test2'); rules_clear_cache(); $this->drupalGet('node/' . $node->nid); $this->assertEqual($this->getUrl(), url('user', array('absolute' => TRUE)), 'Redirect has been issued.'); // Test sending mail. $settings = array('to' => 'mail@example.com', 'subject' => 'subject', 'message' => 'hello.'); rules_action('mail', $settings)->execute(); $this->assertMail('to', 'mail@example.com', 'Mail has been sent.'); $this->assertMail('from', variable_get('site_mail', ini_get('sendmail_from')), 'Default from address has been used'); rules_action('mail', $settings + array('from' => 'sender@example.com'))->execute(); $this->assertMail('from', 'sender@example.com', 'Specified from address has been used'); // Test sending mail to all users of a role. First make sure there is a // custom role and a user for it. $user = $this->drupalCreateUser(array('administer nodes')); $roles = array_keys($user->roles); rules_action('mail_to_users_of_role', $settings + array('roles' => $roles))->execute(); $this->assertMail('to', $user->mail, 'Mail to users of a role has been sent.'); } /** * Tests the path module integration. */ function testPathIntegration() { rules_action('path_alias')->execute('foo', 'bar'); $path = path_load('foo'); $this->assertTrue($path['alias'] == 'bar', 'URL alias has been created.'); $alias_exists = rules_condition('path_alias_exists', array('alias' => 'bar'))->execute(); $this->assertTrue($alias_exists, 'Created URL alias exists.'); $has_alias = rules_condition('path_has_alias', array('source' => 'foo'))->execute(); $this->assertTrue($has_alias, 'System path has an alias.'); // Test node alias action. $node = $this->drupalCreateNode(); rules_action('node_path_alias')->execute($node, 'test'); $path = path_load("node/$node->nid"); $this->assertTrue($path['alias'] == 'test', 'Node URL alias has been created.'); // Test term alias action. $term = entity_property_values_create_entity('taxonomy_term', array( 'name' => $this->randomName(), 'vocabulary' => 1, ))->value(); rules_action('taxonomy_term_path_alias')->execute($term, 'term-test'); $path = path_load("taxonomy/term/$term->tid"); $this->assertTrue($path['alias'] == 'term-test', 'Term URL alias has been created.'); RulesLog::logger()->checkLog(); } }