diff --git a/infolog/inc/class.infolog_ui.inc.php b/infolog/inc/class.infolog_ui.inc.php index bdeeb802a3..ffc868f5d1 100644 --- a/infolog/inc/class.infolog_ui.inc.php +++ b/infolog/inc/class.infolog_ui.inc.php @@ -2220,7 +2220,7 @@ class infolog_ui $content['info_anz_subs'] = (int)$content['info_anz_subs']; // gives javascript error if empty! - $old_pm_id = is_array($pm_links) ? array_shift($pm_links) : $content['old_pm_id']; + $old_pm_id = $content['pm_id'] ?: (is_array($pm_links) ? array_shift($pm_links) : $content['old_pm_id']); unset($content['old_pm_id']); if ($info_id && $this->bo->history) diff --git a/infolog/tests/DoubleLinkPMTest.php b/infolog/tests/DoubleLinkPMTest.php new file mode 100644 index 0000000000..a548a31317 --- /dev/null +++ b/infolog/tests/DoubleLinkPMTest.php @@ -0,0 +1,312 @@ +ui = new \infolog_ui(); + + $this->ui->tmpl = $this->createPartialMock(Etemplate::class, array('exec')); + $this->ui->tmpl->expects($this->any()) + ->method('exec') + ->will($this->returnCallback([$this, 'mockExec'])); + + $this->bo = $this->ui->bo; + $this->pm_bo = new \projectmanager_bo(); + + $this->bo->tracking = $this->createStub(\infolog_tracking::class); + $this->bo->tracking->method('track')->willReturn(0); + + // Make sure projects are not there first + $pm_numbers = array( + 'TEST 1', + 'TEST 2' + ); + foreach($pm_numbers as $number) + { + $project = $this->pm_bo->read(array('pm_number' => $number)); + if($project && $project['pm_id']) + { + $this->pm_bo->delete($project); + } + } + + $this->makeProject("1"); + + // Make another project, we need 2 + $this->pm_bo->data = array(); + $this->makeProject("2"); + } + + protected function tearDown() : void + { + // Remove infolog under test + if($this->info_id) + { + $this->bo->delete($this->info_id, False, False, True); + // One more time for history + $this->bo->delete($this->info_id, False, False, True); + } + + // Remove the test project + $this->deleteProject(); + + $this->bo = null; + $this->pm_bo = null; + + parent::tearDown(); + } + + + /** + * Have an infolog that is part of a project, but then add it into another project. + * We expect both projects to stay linked, but _the_ project to be unchanged. + * It works correctly if you debug it but not if you run it + */ + public function testInProjectLinkAnother() + { + $first_project = $this->pm_id[0]; + $second_project = $this->pm_id[1]; + + $info = $this->getTestInfolog(); + // Set project by ID + //$info['pm_id'] = $first_project; + + $this->info_id = $this->bo->write($info); + $this->assertIsInt($this->info_id); + $this->assertGreaterThan(0, $this->info_id); + + // Force links to run notification now so we get valid testing - it + // usually waits until Egw::on_shutdown(); + Link::run_notifies(); + + // Fake opening the edit dialog, important not to pass an array to accurately copy normal behaviour + $this->ui->edit($this->info_id); + // Set button 'apply' to save, but not try to close the window since + // that would fail + $content = self::$mocked_exec_result; + + $content['pm_id'] = $first_project; + $content['button'] = array('apply' => true); + $this->ui->edit($content); + + // Force links to run notification now so we get valid testing - it + // usually waits until Egw::on_shutdown(); + Link::run_notifies(); + // Now load it again + $info = $this->bo->read($this->info_id); + + // Check original pm_id is there + $this->assertNotNull($info['pm_id'], 'Project was not set'); + $this->assertEquals($first_project, $info['pm_id'], 'Project went missing'); + + // Now link another project + Link::link('infolog', $this->info_id, 'projectmanager', $second_project, "This is the second project"); + + // Force links to run notification now so we get valid testing - it + // usually waits until Egw::on_shutdown(); + Link::run_notifies(); + $this->check_links($this->info_id, $first_project, $second_project); + + // Make a call to edit, looks like user updated subject and clicked Apply + // Ticket #63114 says an edit will remove the second project + $this->loadAndCheckLinks($info, $first_project, $second_project); + + // Check infolog is in original project + $this->checkElements($first_project); + // Check infolog is in second project + $this->checkElements($second_project); + } + + public function mockExec($method, $content, $sel_options, $readonlys, $preserve) + { + $tmpl = new Etemplate('infolog.edit'); + $GLOBALS['egw']->categories = new Categories('', 'infolog'); + $result = parent::mockedExec($tmpl, $content, $sel_options, $readonlys, $preserve); + // Check for the load + $data = array(); + foreach($result as $command) + { + if($command['type'] == 'et2_load') + { + $data = $command['data']; + break; + } + } + + Etemplate::ajax_process_content($data['data']['etemplate_exec_id'], $data['data']['content'], true); + + $content = static::$mocked_exec_result; + + return $content; + } + + /** + * Load infolog via etemplate & check links & pm_id are still there + * + * @param $pm_id_1 + * @param $pm_id_2 + */ + protected function loadAndCheckLinks($info, $pm_id_1, $pm_id_2) + { + // Fake opening the edit dialog, important not to pass an array to accurately copy normal behaviour + $this->ui->edit($info['info_id']); + + // Set button 'apply' to save, but not try to close the window since + // that would fail + $content = self::$mocked_exec_result; + + $content['info_subject'] .= " save #1"; + $content['button'] = array('apply' => true); + $this->ui->edit($content); + + // Now do it again + // Ticket #63114 says a second edit will remove the second project + $content = self::$mocked_exec_result; + + $content['info_subject'] .= " save #2"; + $content['button'] = array('apply' => true); + $this->ui->edit($content); + + $info = $this->bo->read($this->info_id); + + // Check original pm_id is there + $this->assertNotNull($info['pm_id'], 'Original project was not set'); + $this->assertEquals($pm_id_1, $info['pm_id'], 'Original project went missing'); + + $this->check_links($this->info_id, $pm_id_1, $pm_id_2); + } + + protected function check_links($info_id, $pm_id_1, $pm_id_2) + { + // Check links + $links = Link::get_links('infolog', $info_id); + $all_there = count(array_filter($links, function ($link) use ($pm_id_1) + { + return $link['id'] == $pm_id_1; + }) + ) == 1; + $this->assertTrue($all_there, "First project went missing"); + $all_there = count(array_filter($links, function ($link) use ($pm_id_2) + { + return $link['id'] == $pm_id_2; + }) + ) == 1; + $this->assertTrue($all_there, "Second project went missing"); + } + + protected function getTestInfolog() + { + return array( + 'info_subject' => 'Test Infolog Entry for ' . $this->getName() + ); + } + + /** + * Make a project so we can test deleting it + */ + protected function makeProject($pm_number = '') + { + $project = array( + 'pm_number' => 'TEST' . ($pm_number ? " $pm_number" : ''), + 'pm_title' => 'Auto-test for ' . $this->getName(), + 'pm_status' => 'active', + 'pm_description' => 'Test project for ' . $this->getName() + ); + + // Save & set modifier, no notifications + try + { + $result = true; + $result = $this->pm_bo->save($project, true, false); + } + catch (\Exception $e) + { + // Something went wrong, we'll just fail + $this->fail($e); + } + + $this->assertFalse((boolean)$result, 'Error making test project'); + $this->assertArrayHasKey('pm_id', $this->pm_bo->data, 'Could not make test project'); + $this->assertThat($this->pm_bo->data['pm_id'], + $this->logicalAnd( + $this->isType('integer'), + $this->greaterThan(0) + ) + ); + $this->pm_id[] = $this->pm_bo->data['pm_id']; + } + + /** + * Check that the project element is present + * + */ + protected function checkElements($pm_id, $expected_count = 1) + { + $element_bo = new \projectmanager_elements_bo(); + $element_count = 0; + + foreach((array)$element_bo->search(array('pm_id' => $pm_id), false) as $element) + { + $element_count++; + $this->assertEquals($this->info_id, $element['pe_app_id']); + } + + $this->assertEquals($expected_count, $element_count, "Incorrect number of elements"); + } + + /** + * Fully delete a project and its elements, no matter what state or settings + */ + protected function deleteProject() + { + // Force links to run notification now, or elements might stay + // usually waits until Egw::on_shutdown(); + Link::run_notifies(); + + // Force to ignore setting + $this->pm_bo->history = ''; + foreach($this->pm_id as $pm_id) + { + $this->pm_bo->delete($pm_id, true); + } + + // Force links to run notification now, or elements might stay + // usually waits until Egw::on_shutdown(); + Link::run_notifies(); + } + +} \ No newline at end of file