/ */ class Workflow_Test_WorkflowTest extends ModuleTest { /** * Test accessors. */ public function testAccessors() { // add workflow record $modelData = array( 'id' => "test", 'label' => "test label", 'description' => "test description", 'states' => "[foo]\nlabel=foo state\n[bar]\nlabel=bar state" ); Workflow_Model_Workflow::store($modelData); // fetch saved workflow $workflow = Workflow_Model_Workflow::fetch('test'); $this->assertSame( $modelData['label'], $workflow->getLabel(), "Expected workflow label name." ); $this->assertSame( $modelData['description'], $workflow->getDescription(), "Expected workflow description." ); $this->assertSame( $modelData['states'], $workflow->getStatesAsIni(), "Expected workflow states in INI." ); // getStates() should return array of state definitions $states = $workflow->getStates(); $this->assertTrue( is_array($states), "Expected array returned by getState() method." ); // getStateModels() should return iterator with workflow state objects $states = $workflow->getStateModels(); $this->assertTrue( $states instanceof P4Cms_Model_Iterator, "Expected class type returned by getState() method." ); $this->assertEquals( 2, $states->count(), "Expected number of workflow states." ); foreach ($states as $state) { $this->assertTrue( $state instanceof Workflow_Model_State, "Expected class type of workflow state item." ); } } /** * Test mutators. */ public function testMutators() { $workflow = new Workflow_Model_Workflow(array('id' => 'foo')); $workflow->setLabel('foo label') ->setDescription('foo description') ->save(); // fetch and verify values $values = Workflow_Model_Workflow::fetch('foo')->getValues(); $this->assertSame( 'foo', $values['id'], "Expected workflow id." ); $this->assertSame( 'foo label', $values['label'], "Expected workflow label." ); $this->assertSame( 'foo description', $values['description'], "Expected workflow description." ); $this->assertTrue( is_array($values['states']), "Expected type of states field #1." ); // set states as string in INI format $workflow->setStates("[bar]\nlabel=test") ->save(); // fetch and verify states $values = Workflow_Model_Workflow::fetch('foo')->getValues(); $this->assertTrue( is_array($values['states']), "Expected type of states field #2." ); $this->assertEquals( 1, count($values['states']), "Expected number of workflow states #2." ); // verify that workflow state has reference to workflow $state = $workflow->getStateModels()->current(); $this->assertTrue( $state->getValue('workflow') instanceof Workflow_Model_Workflow, "Expected type of state workflow field #2." ); $this->assertSame( 'foo', $state->getValue('workflow')->getId(), "Expected workflow state references correct workflow instance #2." ); // set states as null $workflow->setStates(null) ->save(); // fetch and verify states $values = Workflow_Model_Workflow::fetch('foo')->getValues(); $this->assertTrue( is_array($values['states']), "Expected type of states field." ); $this->assertSame( array(), $values['states'], "Expected no states." ); // set states as array $statesArray = array( 'bar' => array( 'label' => 'bar state', 'foo' => 'temp' ), 'baz' => array( 'label' => 'bazz' ) ); $workflow->setStates($statesArray) ->save(); // fetch and verify states $values = Workflow_Model_Workflow::fetch('foo')->getValues(); $this->assertTrue( is_array($values['states']), "Expected type of states field #3." ); $this->assertEquals( 2, count($values['states']), "Expected number of workflow states #3." ); // verify that workflow states have reference to workflow foreach ($workflow->getStateModels() as $state) { $this->assertTrue( $state->getValue('workflow') instanceof Workflow_Model_Workflow, "Expected type of state workflow field #3 ({$state->getId()})." ); $this->assertSame( 'foo', $state->getValue('workflow')->getId(), "Expected workflow state references correct workflow instance #3 ({$state->getId()})." ); } } /** * Test hasState()/getState()/getStateModel() methods. */ public function testGetState() { Workflow_Model_Workflow::store( array( 'id' => "test", 'label' => "test label", 'description' => "test description", 'states' => "[foo]\na=b\n[bar]a.1=c\na.2=de\nx=yz" ) ); $workflow = Workflow_Model_Workflow::fetch('test'); $this->assertTrue( $workflow->hasState('foo'), "Expected 'foo' is one of the workflow states." ); $this->assertSame( array('a' => 'b'), $workflow->getState('foo'), "Expected value returned for getState('foo')." ); $this->assertTrue( $workflow->getStateModel('foo') instanceof Workflow_Model_State, "Expected class type of 'foo' state model." ); $this->assertTrue( $workflow->hasState('bar'), "Expected 'bar' is one of the workflow states." ); $this->assertSame( array('a' => array (1 => 'c', 2 => 'de'), 'x' => 'yz'), $workflow->getState('bar'), "Expected value returned for getState('bar')." ); $this->assertTrue( $workflow->getStateModel('bar') instanceof Workflow_Model_State, "Expected class type of 'bar' state model." ); $this->assertFalse( $workflow->hasState('noexist'), "Expected 'noexist' is not one of the workflow states." ); try { $workflow->getState('noexist'); $this->fail('Expected throwing an exception when tried to get non-existent state.'); } catch (Workflow_Exception $e) { // expected exception } try { $workflow->getStateModel('noexist'); $this->fail('Expected throwing an exception when tried to get non-existent state model.'); } catch (Workflow_Exception $e) { // expected exception } } /** * Test getDefaultState() method. */ public function testGetDefaultState() { Workflow_Model_Workflow::store( array( 'id' => "test1", 'label' => "test label", 'states' => "[first]\n[second]\n[third]\n" ) ); $defaultState = Workflow_Model_Workflow::fetch('test1')->getDefaultState(); $this->assertTrue( $defaultState instanceof Workflow_Model_State, "Expected class type of default state #1." ); $this->assertSame( 'first', $defaultState->getId(), "Expected id of the default state #1." ); Workflow_Model_Workflow::store( array( 'id' => "test2", 'label' => "test label", 'description' => "test description", 'states' => "[one]\ntwo=2\n[two]one=1\ntwo=2" ) ); $defaultState = Workflow_Model_Workflow::fetch('test2')->getDefaultState(); $this->assertTrue( $defaultState instanceof Workflow_Model_State, "Expected class type of default state #2." ); $this->assertSame( 'one', $defaultState->getId(), "Expected id of the default state #2." ); } /** * Test installation and removal of default workflows. */ public function testInstallRemoveDefaults() { $this->assertSame( 0, Workflow_Model_Workflow::count(), "Expected no workflows yet." ); $package = P4Cms_Module::fetch('workflow'); Workflow_Model_Workflow::installPackageDefaults($package); $this->assertSame( 1, Workflow_Model_Workflow::count(), "Expected one workflow." ); // ensure workflow has id 'simple'. $this->assertTrue(Workflow_Model_Workflow::exists('simple')); // verify installed workflow has three states: // Draft, Review, Published $workflow = Workflow_Model_Workflow::fetch('simple'); $this->assertSame( $workflow->getStateModels()->invoke('getLabel'), array('Draft', 'Review', 'Published') ); // remove defaults. Workflow_Model_Workflow::removePackageDefaults($package); $this->assertFalse(Workflow_Model_Workflow::exists('simple')); // ensure workflow is not removed if it was edited. Workflow_Model_Workflow::installPackageDefaults($package); $workflow = Workflow_Model_Workflow::fetch('simple'); $workflow->setLabel('modified')->save(); Workflow_Model_Workflow::removePackageDefaults($package); $this->assertTrue(Workflow_Model_Workflow::exists('simple')); } /** * Verify fetchTypeMap() method outputs correct content-type => workflow mapping. */ public function testFetchTypeMap() { // add few workflows $workflowX = Workflow_Model_Workflow::store( array( 'id' => "workflow-x", 'label' => "test x", 'states' => "[foo]\na=b" ) ); $workflowY = Workflow_Model_Workflow::store( array( 'id' => "workflow-y", 'label' => "test y", 'states' => "[foo]\na=b" ) ); // add few content types P4Cms_Content_Type::store( array( 'id' => "type-a", 'elements' => "[id]\ntype=text", 'workflow' => "workflow-x" ) ); P4Cms_Content_Type::store( array( 'id' => "type-b", 'elements' => "[id]\ntype=text" ) ); P4Cms_Content_Type::store( array( 'id' => "type-c", 'elements' => "[id]\ntype=text", 'workflow' => "workflow-y" ) ); P4Cms_Content_Type::store( array( 'id' => "type-d", 'elements' => "[id]\ntype=text", 'workflow' => "workflow-x" ) ); $map = Workflow_Model_Workflow::fetchTypeMap(); $expected = array( 'type-a' => $workflowX->toArray(), 'type-c' => $workflowY->toArray(), 'type-d' => $workflowX->toArray(), ); $this->assertTrue( $map instanceof P4Cms_Model_Iterator, "Expected map instance type." ); $this->assertSame( $expected, $map->toArray(), "Expected map array structure." ); } /** * Test detectTransitionOn() method. */ public function testDetectTransitionOn() { // create workflow with states a, b, c allowing following transitions: // a->b, a->c, b->c, c->a $workflow = Workflow_Model_Workflow::create( $this->_generateWorkflowDefinition( array( 'a' => array('b', 'c'), 'b' => array('c'), 'c' => array('a') ) ) ); // create content type $type = P4Cms_Content_Type::store( array( 'id' => 'ct', 'elements' => array( 'title' => array('type' => 'text') ) ) ); // define tests suite $tests = array( array( 'savedValues' => array( 'contentType' => $type->getId(), ), 'changedValues' => array( 'workflowState' => 'a' ), 'expected' => null, 'message' => __LINE__ . ": invalid from state, valid to state." ), array( 'savedValues' => array( 'contentType' => $type->getId(), 'workflowState' => 'a' ), 'changedValues' => array( 'workflowState' => 'unknown' ), 'expected' => null, 'message' => __LINE__ . ": valid from state, invalid to state." ), array( 'savedValues' => array( 'contentType' => $type->getId(), 'workflowState' => 'notvalid' ), 'changedValues' => array( 'workflowState' => 'unknown' ), 'expected' => null, 'message' => __LINE__ . ": invalid from state, invalid to state." ), array( 'savedValues' => array( 'contentType' => $type->getId(), 'workflowState' => 'a' ), 'changedValues' => array( 'workflowState' => 'b' ), 'expected' => 'a to b', 'message' => __LINE__ . ": valid transiton a -> b." ), array( 'savedValues' => array( 'contentType' => $type->getId(), 'workflowState' => 'a' ), 'changedValues' => array( 'workflowState' => 'a' ), 'expected' => null, 'message' => __LINE__ . ": same from- and to states." ) ); // run tests foreach ($tests as $test) { // create content $content = P4Cms_Content::store($test['savedValues']); // modify record values and save while in batch $adapter = $content->getAdapter(); $adapter->beginBatch('test'); $content->setValues($test['changedValues']); $content->save(); // detect transition $transition = $workflow->detectTransitionOn($content); // get transition label if transition is object if ($transition instanceof Workflow_Model_Transition) { $transition = $transition->getLabel(); } // verify transition $this->assertSame( $test['expected'], $transition, $test['message'] ); $adapter->revertBatch(); } } /** * Test (set|get)StateOf() methods. */ public function testGetSetStateOf() { // create workflow states a,b,c and with transitions a->b, a->c, b->c $workflow = Workflow_Model_Workflow::create( $this->_generateWorkflowDefinition( array( 'a' => array('b', 'c'), 'b' => array('c'), 'c' => array() ) ) ); // create content type $type = P4Cms_Content_Type::store( array( 'id' => 'test', 'elements' => array( 'title' => array('type' => 'text') ) ) ); $record = P4Cms_Content::create( array( 'contentType' => 'test', 'workflowState' => 'b' ) ); // ensure exception is thrown when try to set invalid state try { $workflow->setStateOf($record, 'none'); } catch (Workflow_Exception $e) { // expected exception $this->assertSame( "Cannot set state on the given record. State is undefined or governed by other workflow.", $e->getMessage(), "Expected invalid state exception" ); } catch (Exception $e) { $this->fail("Unexpected exception thrown."); } $this->assertSame( 'b', $workflow->getStateOf($record)->getId(), "Expected current state." ); // ensure exception is thrown when invalid transition try { $workflow->setStateOf($record, 'a'); } catch (Workflow_Exception $e) { // expected exception $this->assertSame( "Cannot set state on the given record. Not a valid transition.", $e->getMessage(), "Expected invalid transition exception" ); } catch (Exception $e) { $this->fail("Unexpected exception thrown."); } $this->assertSame( 'b', $workflow->getStateOf($record)->getId(), "Expected current state." ); // set valid state $workflow->setStateOf($record, 'c'); $this->assertSame( 'c', $workflow->getStateOf($record)->getId(), "Expected current state." ); } /** * Test getters/setters for manipulation with scheduled data. */ public function testGetSetScheduledData() { $this->utility->impersonate('editor'); // create workflow states a,b,c and with transitions a->b, a->c, b->c $workflow = Workflow_Model_Workflow::create( $this->_generateWorkflowDefinition( array( 'a' => array('b', 'c'), 'b' => array('c'), 'c' => array() ) ) ); $record = P4Cms_Content::store( array( 'id' => 1, 'contentType' => 'test', 'workflowState' => 'a' ) ); // ensure exception is thrown when try to set invalid scheduled state try { $workflow->setScheduledStateOf($record, 'none', 123); } catch (Workflow_Exception $e) { // expected exception $this->assertSame( "Cannot set state on the given record. State is undefined or governed by other workflow.", $e->getMessage(), "Expected invalid state exception" ); } catch (Exception $e) { $this->fail("Unexpected exception thrown."); } // prepare timestamp points to the future $time = time() + 10; // ensure exception is thrown when timestamp has invalid format try { $workflow->setScheduledStateOf($record, 'b', "$time"); } catch (InvalidArgumentException $e) { // expected exception $this->assertSame( "Cannot schedule transition. Time must be an integer timestamp in the future.", $e->getMessage(), "Expected invalid timestamp exception" ); } catch (Exception $e) { $this->fail("Unexpected exception thrown."); } try { $workflow->setScheduledStateOf($record, 'b', -1); } catch (InvalidArgumentException $e) { // expected exception $this->assertSame( "Cannot schedule transition. Time must be an integer timestamp in the future.", $e->getMessage(), "Expected invalid timestamp exception" ); } catch (Exception $e) { $this->fail("Unexpected exception thrown."); } // set valid scheduled transition $workflow->setScheduledStateOf($record, 'b', $time); $record->save(); $record->fetch(1); $this->assertSame('a', $workflow->getStateOf($record)->getId(), 'Expected current state'); $this->assertSame('b', $workflow->getScheduledStateOf($record)->getId(), 'Expected scheduled state'); $this->assertSame($time, $workflow->getScheduledTimeOf($record), 'Expected scheduled time'); } /** * Test for makeScheduledContentFilter() method. */ public function testMakeScheduledContentFilter() { // these are unpublished entries, impersonate an editor so we can see them $this->utility->impersonate('editor'); // create testing entries // +---------------+--------------+-----------------+-------------------+ // | CONTENT TITLE | STATE | SCHEDULED STATE | SCHEDULED TIME | // +---------------+--------------+-----------------+-------------------+ // | a | draft | review | 1. 1.2010 12:00 | // | b | draft | published | 1. 2.2010 15:00 | // | c | draft | review | 1. 1.2011 12:00 | // | d | draft | - | 31.12.2009 23:00 | // | e | draft | published | - | // | f | draft | - | - | // | g | draft | review | 2. 1.2011 12:00 | // | h | - | published | 2. 1.2012 12:00 | // +---------------+--------------+-----------------+-------------------+ $entries = array( array('a', 'draft', 'review', strtotime('2010-01-01 12:00')), array('b', 'draft', 'published', strtotime('2010-02-01 15:00')), array('c', 'draft', 'review', strtotime('2011-01-01 12:00')), array('d', 'draft', '', strtotime('2009-12-31 23:00')), array('e', 'draft', 'published', ''), array('f', 'draft', '', ''), array('g', 'draft', 'review', strtotime('2011-01-02 12:00')), array('h', '', 'published', strtotime('2012-01-02 12:00')), ); // make a test workflow and type $workflow = Workflow_Model_Workflow::store( $this->_generateWorkflowDefinition( array('draft' => array('published'), 'published' => array('draft')) ) ); $type = new P4Cms_Content_Type; $type->setId('basic') ->setValue('workflow', $workflow->getId()) ->save(); foreach ($entries as $entry) { P4Cms_Content::store( array( 'contentType' => 'basic', 'title' => $entry[0], 'workflowState' => $entry[1], 'workflowScheduledState' => $entry[2], 'workflowScheduledTime' => $entry[3] ) ); } // get all contents with scheduled transitions before 1999-01-01 00:00 $query = P4Cms_Record_Query::create() ->addFilter( Workflow_Model_Workflow::makeScheduledContentFilter(strtotime('1999-01-01 00:00')) ); $result = P4Cms_Content::fetchAll($query)->sortBy('title')->invoke('getTitle'); $this->assertSame( array(), $result, 'Expected entries filtered by scheduled transitions #1.' ); // get all contents with scheduled transitions before 2010-02-01 14:59 $query = P4Cms_Record_Query::create() ->addFilter( Workflow_Model_Workflow::makeScheduledContentFilter(strtotime('2010-02-01 14:59')) ); $result = P4Cms_Content::fetchAll($query)->sortBy('title')->invoke('getTitle'); $this->assertSame( array('a'), $result, 'Expected entries filtered by scheduled transitions #2.' ); // get all contents with scheduled transitions before 2011-01-01 00:00 $query = P4Cms_Record_Query::create() ->addFilter( Workflow_Model_Workflow::makeScheduledContentFilter(strtotime('2011-01-01 00:00')) ); $result = P4Cms_Content::fetchAll($query)->sortBy('title')->invoke('getTitle'); $this->assertSame( array('a', 'b'), $result, 'Expected entries filtered by scheduled transitions #3.' ); // get all contents with scheduled transitions before 2012-01-01 23:00 $query = P4Cms_Record_Query::create() ->addFilter( Workflow_Model_Workflow::makeScheduledContentFilter(strtotime('2012-01-01 23:00')) ); $result = P4Cms_Content::fetchAll($query)->sortBy('title')->invoke('getTitle'); $this->assertSame( array('a', 'b', 'c', 'g'), $result, 'Expected entries filtered by scheduled transitions #4.' ); // get all contents with scheduled transitions before 2025-01-01 17:45 $query = P4Cms_Record_Query::create() ->addFilter( Workflow_Model_Workflow::makeScheduledContentFilter(strtotime('2025-01-01 17:45')) ); $result = P4Cms_Content::fetchAll($query)->sortBy('title')->invoke('getTitle'); $this->assertSame( array('a', 'b', 'c', 'g', 'h'), $result, 'Expected entries filtered by scheduled transitions #5.' ); } /** * Verify that content entries with an invalid content type are unpublished */ public function testInvalidTypeIsUnpublished() { P4Cms_Content::store( array( 'contentType' => 'made-up', 'title' => 'so fake', 'workflowState' => Workflow_Model_State::PUBLISHED ) ); $this->assertSame( 0, P4Cms_Content::count(), 'expected no hits when type does not exist' ); // now add the type and try it with only the workflow missing $type = new P4Cms_Content_Type; $type->setId('made-up') ->setValue('workflow', 'fake') ->save(); $this->assertSame( 0, P4Cms_Content::count(), 'expected no hits when workflow does not exist' ); // try with all items but no published state $workflow = new Workflow_Model_Workflow; $workflow->setId('fake') ->setValues($this->_generateWorkflowDefinition(array('draft' => array('review')))) ->save(); $this->assertSame( 0, P4Cms_Content::count(), 'expected no hits when workflow does not have a published state' ); // test it works when we have a published state $workflow->setValues($this->_generateWorkflowDefinition(array('published' => array('super-published')))) ->save(); $this->assertSame( 1, P4Cms_Content::count(), 'expected a hit when everything is in place' ); } /** * Helper method to generate simplified workflow definition. * * @param array $stateTransitions list with state transitions * @return array simplified workflow definition */ protected function _generateWorkflowDefinition(array $stateTransitions) { $definition = array(); foreach ($stateTransitions as $state => $transitions) { $data = array( 'label' => "$state state", ); foreach ($transitions as $transition) { $data['transitions'][$transition] = array( 'label' => "$state to $transition" ); } $definition['states'][$state] = $data; } return $definition; } }