* @copyright 2021 Uwe Steinmann * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @version @package_version@ * @link https://www.seeddms.org */ use PHPUnit\Framework\SeedDmsTest; /** * Group test class * * @category SeedDMS * @package Tests * @author Uwe Steinmann * @copyright 2021 Uwe Steinmann * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @version Release: @package_version@ * @link https://www.seeddms.org */ class WorkflowTest extends SeedDmsTest { /** * Create a real sqlite database in memory * * @return void */ protected function setUp(): void { self::$dbh = self::createInMemoryDatabase(); self::$contentdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'phpunit-'.time(); mkdir(self::$contentdir); // echo "Creating temp content dir: ".self::$contentdir."\n"; self::$dms = new \SeedDMS_Core_DMS(self::$dbh, self::$contentdir); self::$dbversion = self::$dms->getDBVersion(); } /** * Clean up at tear down * * @return void */ protected function tearDown(): void { self::$dbh = null; // echo "\nRemoving temp. content dir: ".self::$contentdir."\n"; exec('rm -rf '.self::$contentdir); } /** * Test method getInitState() and setInitState() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testGetAndSetInitState() { $ws_nr = self::$dms->addWorkflowState('needs review', S_IN_WORKFLOW); $ws_na = self::$dms->addWorkflowState('needs approval', S_IN_WORKFLOW); $workflow = self::$dms->addWorkflow('traditional workflow', $ws_nr); $initstate = $workflow->getInitState(); $this->assertEquals($ws_nr->getName(), $initstate->getName()); $ret = $workflow->setInitState($ws_na); $this->assertTrue($ret); $initstate = $workflow->getInitState(); $this->assertEquals($ws_na->getName(), $initstate->getName()); } /** * Test method getName() and setName() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testGetAndSetStateName() { $state = self::$dms->addWorkflowState('needs review', S_IN_WORKFLOW); $name = $state->getName(); $this->assertEquals('needs review', $name); $ret = $state->setName('foobar'); $this->assertTrue($ret); $name = $state->getName(); $this->assertEquals('foobar', $name); } /** * Test method getName() and setName() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testGetAndSetActionName() { $action = self::$dms->addWorkflowAction('action'); $name = $action->getName(); $this->assertEquals('action', $name); $ret = $action->setName('foobar'); $this->assertTrue($ret); $name = $action->getName(); $this->assertEquals('foobar', $name); } /** * Test method getName() and setName() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testGetAndSetWorkflowName() { $ws_nr = self::$dms->addWorkflowState('needs review', S_IN_WORKFLOW); $workflow = self::$dms->addWorkflow('traditional workflow', $ws_nr); $name = $workflow->getName(); $this->assertEquals('traditional workflow', $name); $ret = $workflow->setName('foo'); $this->assertTrue($ret); $name = $workflow->getName(); $this->assertEquals('foo', $name); } /** * Test method getDocumentStatus() and setDocumentStatus() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testGetAndSetDocumentStatus() { $state = self::$dms->addWorkflowState('some name', S_RELEASED); $docstatus = $state->getDocumentStatus(); $this->assertEquals(S_RELEASED, $docstatus); $ret = $state->setDocumentStatus(S_REJECTED); $this->assertTrue($ret); $docstatus = $state->getDocumentStatus(); $this->assertEquals(S_REJECTED, $docstatus); } /** * Test method workflow->remove() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testCreateAndRemoveWorkflow() { $rootfolder = self::$dms->getRootFolder(); $user = self::$dms->getUser(1); $this->assertIsObject($user); /* Add a new user who will be the reviewer */ $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($reviewer); /* Add a new user who will be the approver */ $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($approver); $workflow = self::createWorkflow($reviewer, $approver); $this->assertIsObject($workflow); $ret = $workflow->remove(); $this->assertTrue($ret); $states = self::$dms->getAllWorkflowStates(); $this->assertIsArray($states); $this->assertCount(4, $states); foreach($states as $state) $this->assertFalse($state->isUsed()); $actions = self::$dms->getAllWorkflowActions(); $this->assertIsArray($actions); $this->assertCount(3, $actions); foreach($actions as $action) $this->assertFalse($action->isUsed()); } /** * Test method remove() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testCreateAndRemoveAction() { $action = self::$dms->addWorkflowAction('action'); $this->assertIsObject($action); $actions = self::$dms->getAllWorkflowActions(); $this->assertIsArray($actions); $this->assertCount(1, $actions); $ret = $action->remove(); $this->assertTrue($ret); $actions = self::$dms->getAllWorkflowActions(); $this->assertIsArray($actions); $this->assertCount(0, $actions); } /** * Test method remove() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testCreateAndRemoveState() { $state = self::$dms->addWorkflowState('needs review', S_IN_WORKFLOW); $this->assertIsObject($state); $states = self::$dms->getAllWorkflowStates(); $this->assertIsArray($states); $this->assertCount(1, $states); $ret = $state->remove(); $this->assertTrue($ret); $states = self::$dms->getAllWorkflowStates(); $this->assertIsArray($states); $this->assertCount(0, $states); } /** * Test method setWorkflow(), getWorkflow(), getWorkflowState() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testAssignWorkflow() { $rootfolder = self::$dms->getRootFolder(); $user = self::$dms->getUser(1); $this->assertIsObject($user); /* Add a new user who will be the reviewer */ $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($reviewer); /* Add a new user who will be the approver */ $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($approver); $workflow = self::createWorkflow($reviewer, $approver); $this->assertIsObject($workflow); /* Check for cycles */ $cycles = $workflow->checkForCycles(); $this->assertFalse($cycles); /* Add a new document */ $document = self::createDocument($rootfolder, $user, 'Document 1'); $content = $document->getLatestContent(); $this->assertIsObject($content); $status = $content->getStatus(); $this->assertIsArray($status); $this->assertEquals(S_RELEASED, $status['status']); /* Assign the workflow */ $ret = $content->setWorkflow($workflow, $user); $this->assertTrue($ret); /* Assign a workflow again causes an error */ $ret = $content->setWorkflow($workflow, $user); $this->assertFalse($ret); /* Get a fresh copy of the content from the database and get the workflow */ $again = self::$dms->getDocumentContent($content->getId()); $this->assertIsObject($again); $w = $again->getWorkflow(); $this->assertEquals($workflow->getId(), $w->getId()); /* Status of content should be S_IN_WORKFLOW now */ $status = $content->getStatus(); $this->assertIsArray($status); $this->assertEquals(S_IN_WORKFLOW, $status['status']); /* Get current workflow state */ $state = $content->getWorkflowState(); $this->assertEquals('needs review', $state->getName()); $workflowlog = $content->getWorkflowLog(); $this->assertIsArray($workflowlog); $this->assertCount(0, $workflowlog); /* The workflow has altogether 4 states */ $states = $workflow->getStates(); $this->assertIsArray($states); $this->assertCount(4, $states); /* Check the initial state */ $initstate = $workflow->getInitState(); $this->assertEquals('needs review', $initstate->getName()); /* init state is definitely used */ $ret = $initstate->isUsed(); $this->assertTrue($ret); /* init state has two transistions linked to it */ $transitions = $initstate->getTransitions(); $this->assertIsArray($transitions); $this->assertCount(2, $transitions); /* Check if workflow is used by any document */ $isused = $workflow->isUsed(); $this->assertTrue($isused); } /** * Test method setWorkflow(), getWorkflow(), getWorkflowState() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testStepThroughWorkflow() { $rootfolder = self::$dms->getRootFolder(); $user = self::$dms->getUser(1); $this->assertIsObject($user); /* Add a new user who will be the reviewer */ $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($reviewer); /* Add a new user who will be the approver */ $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($approver); $workflow = self::createWorkflow($reviewer, $approver); /* Add a new document */ $document = self::createDocument($rootfolder, $user, 'Document 1'); $content = $document->getLatestContent(); $this->assertIsObject($content); $status = $content->getStatus(); $this->assertIsArray($status); $this->assertEquals(S_RELEASED, $status['status']); /* Assign the workflow */ $ret = $content->setWorkflow($workflow, $user); $this->assertTrue($ret); $status = $content->getStatus(); $this->assertIsArray($status); $this->assertEquals(S_IN_WORKFLOW, $status['status']); /* Remove the workflow */ $ret = $content->removeWorkflow($user); $this->assertTrue($ret); $status = $content->getStatus(); $this->assertIsArray($status); $this->assertEquals(S_RELEASED, $status['status']); /* Remove the workflow again is just fine */ $ret = $content->removeWorkflow($user); $this->assertTrue($ret); /* Assign the workflow again */ $ret = $content->setWorkflow($workflow, $user); $this->assertTrue($ret); $status = $content->getStatus(); $this->assertIsArray($status); $this->assertEquals(S_IN_WORKFLOW, $status['status']); /* Check if workflow needs action by the reviewer/approver */ $ret = $content->needsWorkflowAction($reviewer); $this->assertTrue($ret); $ret = $content->needsWorkflowAction($approver); $this->assertFalse($ret); /* Get current workflow state*/ $state = $content->getWorkflowState(); $this->assertEquals('needs review', $state->getName()); /* There should be two possible transitions now * NR -- review -> NA * NR -- reject -> RJ */ $nexttransitions = $workflow->getNextTransitions($state); $this->assertIsArray($nexttransitions); $this->assertCount(2, $nexttransitions); /* But of course, there were no previous transitions */ $prevtransitions = $workflow->getPreviousTransitions($state); $this->assertIsArray($prevtransitions); $this->assertCount(0, $prevtransitions); /* Check if reviewer is allowed to trigger the transition. * As we are still in the intitial state, the possible transitions * may both be triggered by the reviewer but not by the approver. */ foreach($nexttransitions as $nexttransition) { if($nexttransition->getNextState()->getDocumentStatus() == S_REJECTED) $rejecttransition = $nexttransition; elseif($nexttransition->getNextState()->getDocumentStatus() == S_IN_WORKFLOW) $reviewtransition = $nexttransition; $ret = $content->triggerWorkflowTransitionIsAllowed($reviewer, $nexttransition); $this->assertTrue($ret); $ret = $content->triggerWorkflowTransitionIsAllowed($approver, $nexttransition); $this->assertFalse($ret); } /* Trigger the successful review transition. * As there is only one reviewer the transition will fire and the workflow * moves forward into the next state. triggerWorkflowTransition() returns the * next state. */ $nextstate = $content->triggerWorkflowTransition($reviewer, $reviewtransition, 'Review succeeded'); $this->assertIsObject($nextstate); $this->assertEquals('needs approval', $nextstate->getName()); $state = $content->getWorkflowState(); $this->assertEquals($nextstate->getId(), $state->getId()); $this->assertEquals('needs approval', $state->getName()); /* The workflow log has one entry now */ $workflowlog = $content->getLastWorkflowLog(); $this->assertIsObject($workflowlog); $this->assertEquals('Review succeeded', $workflowlog->getComment()); /* There should be two possible transitions now * NA -- approve -> RL * NA -- reject -> RJ */ $nexttransitions = $workflow->getNextTransitions($state); $this->assertIsArray($nexttransitions); $this->assertCount(2, $nexttransitions); /* But of course, there is one previous transitions, the one that led to * the current state of the workflow. */ $prevtransitions = $workflow->getPreviousTransitions($state); $this->assertIsArray($prevtransitions); $this->assertCount(1, $prevtransitions); $this->assertEquals($reviewtransition->getId(), $prevtransitions[0]->getId()); /* Check if approver is allowed to trigger the transition. * As we are now in 'needs approval' state, the possible transitions * may both be triggered by the approver but not by the reviewer. */ foreach($nexttransitions as $nexttransition) { if($nexttransition->getNextState()->getDocumentStatus() == S_REJECTED) $rejecttransition = $nexttransition; elseif($nexttransition->getNextState()->getDocumentStatus() == S_RELEASED) $releasetransition = $nexttransition; $ret = $content->triggerWorkflowTransitionIsAllowed($approver, $nexttransition); $this->assertTrue($ret); $ret = $content->triggerWorkflowTransitionIsAllowed($reviewer, $nexttransition); $this->assertFalse($ret); } /* Trigger the successful approve transition. * As there is only one approver the transition will fire and the workflow * moves forward into the next state. triggerWorkflowTransition() returns the * next state. */ $nextstate = $content->triggerWorkflowTransition($approver, $releasetransition, 'Approval succeeded'); $this->assertIsObject($nextstate); $this->assertEquals('released', $nextstate->getName()); /* The workflow log has two entries now */ $workflowlog = $content->getLastWorkflowLog(); $this->assertIsObject($workflowlog); $this->assertEquals('Approval succeeded', $workflowlog->getComment()); /* Because the workflow has reached a final state, the workflow will no * longer be attached to the document. */ $workflow = $content->getWorkflow(); $this->assertFalse($workflow); /* There is also no way to get the state anymore */ $state = $content->getWorkflowState(); $this->assertFalse($state); $status = $content->getStatus(); $this->assertIsArray($status); $this->assertEquals(S_RELEASED, $status['status']); /* Even after the workflow has been finished the log can still be retrieved */ $workflowlog = $content->getLastWorkflowLog(); $this->assertIsObject($workflowlog); $this->assertEquals('Approval succeeded', $workflowlog->getComment()); } /** * Test method rewindWorkflow() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testRewindWorkflow() { $rootfolder = self::$dms->getRootFolder(); $user = self::$dms->getUser(1); $this->assertIsObject($user); /* Add a new user who will be the reviewer */ $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($reviewer); /* Add a new user who will be the approver */ $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($approver); $workflow = self::createWorkflow($reviewer, $approver); /* Add a new document */ $document = self::createDocument($rootfolder, $user, 'Document 1'); $content = $document->getLatestContent(); $this->assertIsObject($content); $status = $content->getStatus(); $this->assertIsArray($status); $this->assertEquals(S_RELEASED, $status['status']); /* Assign the workflow */ $ret = $content->setWorkflow($workflow, $user); $this->assertTrue($ret); $status = $content->getStatus(); $this->assertIsArray($status); $this->assertEquals(S_IN_WORKFLOW, $status['status']); /* Check if workflow needs action by the reviewer */ $ret = $content->needsWorkflowAction($reviewer); $this->assertTrue($ret); /* Get current workflow state*/ $state = $content->getWorkflowState(); $this->assertEquals('needs review', $state->getName()); /* There should be two possible transitions now * NR -- review -> NA * NR -- reject -> RJ */ $nexttransitions = $workflow->getNextTransitions($state); $this->assertIsArray($nexttransitions); $this->assertCount(2, $nexttransitions); /* Check if reviewer is allowed to trigger the transition. * As we are still in the intitial state, the possible transitions * may both be triggered by the reviewer but not by the approver. */ foreach($nexttransitions as $nexttransition) { if($nexttransition->getNextState()->getDocumentStatus() == S_IN_WORKFLOW) $reviewtransition = $nexttransition; } /* Trigger the successful review transition. * As there is only one reviewer the transition will fire and the workflow * moves forward into the next state. triggerWorkflowTransition() returns the * next state. */ $nextstate = $content->triggerWorkflowTransition($reviewer, $reviewtransition, 'Review succeeded'); $this->assertIsObject($nextstate); $this->assertEquals('needs approval', $nextstate->getName()); /* Get current workflow state*/ $state = $content->getWorkflowState(); $this->assertEquals('needs approval', $state->getName()); /* The workflow log has one entry now */ $workflowlogs = $content->getWorkflowLog(); $this->assertIsArray($workflowlogs); $this->assertCount(1, $workflowlogs); if(self::$dbversion['major'] > 5) $this->assertEquals('Review succeeded', $workflowlogs[1][0]->getComment()); else $this->assertEquals('Review succeeded', $workflowlogs[0]->getComment()); $ret = $content->rewindWorkflow(); $this->assertTrue($ret); /* After rewinding the workflow the initial state is set ... */ $state = $content->getWorkflowState(); $this->assertEquals('needs review', $state->getName()); /* and the workflow log has been cleared */ $workflowlogs = $content->getWorkflowLog(); $this->assertIsArray($workflowlogs); $this->assertCount(0, $workflowlogs); } /** * Test method getTransitionsByStates() * * This method uses a real in memory sqlite3 database. * * @return void */ public function testTransitionsByStateWorkflow() { $rootfolder = self::$dms->getRootFolder(); $user = self::$dms->getUser(1); $this->assertIsObject($user); /* Add a new user who will be the reviewer */ $reviewer = self::$dms->addUser('reviewer', 'reviewer', 'Reviewer One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($reviewer); /* Add a new user who will be the approver */ $approver = self::$dms->addUser('approver', 'approver', 'Approver One', 'user1@seeddms.org', 'en_GB', 'bootstrap', ''); $this->assertIsObject($approver); $workflow = self::createWorkflow($reviewer, $approver); /* Check the initial state */ $initstate = $workflow->getInitState(); $this->assertEquals('needs review', $initstate->getName()); /* init state has two transistions linked to it */ $transitions = $initstate->getTransitions(); $this->assertIsArray($transitions); $this->assertCount(2, $transitions); $t = $workflow->getTransitionsByStates($initstate, $transitions[1]->getNextState()); $this->assertEquals($transitions[1]->getId(), $t[0]->getId()); } }