1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees; 21 22use Fisharebest\Webtrees\Contracts\CacheFactoryInterface; 23use Fisharebest\Webtrees\Contracts\UserInterface; 24use Fisharebest\Webtrees\Services\GedcomExportService; 25use Fisharebest\Webtrees\Services\GedcomImportService; 26use Fisharebest\Webtrees\Services\TreeService; 27use Fisharebest\Webtrees\Services\UserService; 28use InvalidArgumentException; 29use Nyholm\Psr7\Factory\Psr17Factory; 30use PHPUnit\Framework\Attributes\CoversClass; 31use Symfony\Component\Cache\Adapter\NullAdapter; 32 33use function fclose; 34use function file_get_contents; 35use function preg_replace; 36use function stream_get_contents; 37 38 39#[CoversClass(Tree::class)] 40#[CoversClass(TreeService::class)] 41#[CoversClass(GedcomExportService::class)] 42class TreeTest extends TestCase 43{ 44 protected static bool $uses_database = true; 45 46 /** 47 * Things to run before every test. 48 */ 49 protected function setUp(): void 50 { 51 parent::setUp(); 52 53 $cache_factory = $this->createMock(CacheFactoryInterface::class); 54 $cache_factory->method('array')->willReturn(new Cache(new NullAdapter())); 55 Registry::cache($cache_factory); 56 } 57 58 public function testConstructor(): void 59 { 60 $gedcom_import_service = new GedcomImportService(); 61 $tree_service = new TreeService($gedcom_import_service); 62 $tree = $tree_service->create('name', 'title'); 63 64 self::assertSame('name', $tree->name()); 65 self::assertSame('title', $tree->title()); 66 } 67 68 public function testTreePreferences(): void 69 { 70 $gedcom_import_service = new GedcomImportService(); 71 $tree_service = new TreeService($gedcom_import_service); 72 $tree = $tree_service->create('name', 'title'); 73 74 $tree->setPreference('foo', 'bar'); 75 $pref = $tree->getPreference('foo'); 76 self::assertSame('bar', $pref); 77 } 78 79 public function testUserTreePreferences(): void 80 { 81 $user_service = new UserService(); 82 $gedcom_import_service = new GedcomImportService(); 83 $tree_service = new TreeService($gedcom_import_service); 84 $tree = $tree_service->create('name', 'title'); 85 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 86 87 $pref = $tree->getUserPreference($user, 'foo', 'default'); 88 self::assertSame('default', $pref); 89 90 $tree->setUserPreference($user, 'foo', 'bar'); 91 $pref = $tree->getUserPreference($user, 'foo', 'default'); 92 self::assertSame('bar', $pref); 93 } 94 95 public function testCreateInvalidIndividual(): void 96 { 97 $this->expectException(InvalidArgumentException::class); 98 99 $user_service = new UserService(); 100 $gedcom_import_service = new GedcomImportService(); 101 $tree_service = new TreeService($gedcom_import_service); 102 $tree = $tree_service->create('name', 'title'); 103 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 104 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1'); 105 Auth::login($user); 106 107 $tree->createIndividual("0 @@ FOO\n1 SEX U"); 108 } 109 110 public function testCreateIndividual(): void 111 { 112 $user_service = new UserService(); 113 $gedcom_import_service = new GedcomImportService(); 114 $tree_service = new TreeService($gedcom_import_service); 115 $tree = $tree_service->create('name', 'title'); 116 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 117 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1'); 118 Auth::login($user); 119 120 $record = $tree->createIndividual("0 @@ INDI\n1 SEX F\n1 NAME Foo /Bar/"); 121 self::assertTrue($record->isPendingAddition()); 122 123 $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, '1'); 124 $record = $tree->createIndividual("0 @@ INDI\n1 SEX F\n1 NAME Foo /Bar/"); 125 self::assertFalse($record->isPendingAddition()); 126 } 127 128 public function testCreateInvalidFamily(): void 129 { 130 $this->expectException(InvalidArgumentException::class); 131 132 $user_service = new UserService(); 133 $gedcom_import_service = new GedcomImportService(); 134 $tree_service = new TreeService($gedcom_import_service); 135 $tree = $tree_service->create('name', 'title'); 136 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 137 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1'); 138 Auth::login($user); 139 140 $tree->createFamily("0 @@ FOO\n1 MARR Y"); 141 } 142 143 public function testCreateFamily(): void 144 { 145 $user_service = new UserService(); 146 $gedcom_import_service = new GedcomImportService(); 147 $tree_service = new TreeService($gedcom_import_service); 148 $tree = $tree_service->create('name', 'title'); 149 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 150 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1'); 151 Auth::login($user); 152 153 $record = $tree->createFamily("0 @@ FAM\n1 MARR Y"); 154 self::assertTrue($record->isPendingAddition()); 155 156 $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, '1'); 157 $record = $tree->createFamily("0 @@ FAM\n1 MARR Y"); 158 self::assertFalse($record->isPendingAddition()); 159 } 160 161 public function testCreateInvalidMediaObject(): void 162 { 163 $this->expectException(InvalidArgumentException::class); 164 165 $user_service = new UserService(); 166 $gedcom_import_service = new GedcomImportService(); 167 $tree_service = new TreeService($gedcom_import_service); 168 $tree = $tree_service->create('name', 'title'); 169 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 170 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1'); 171 Auth::login($user); 172 173 $tree->createMediaObject("0 @@ FOO\n1 MARR Y"); 174 } 175 176 public function testCreateMediaObject(): void 177 { 178 $user_service = new UserService(); 179 $gedcom_import_service = new GedcomImportService(); 180 $tree_service = new TreeService($gedcom_import_service); 181 $tree = $tree_service->create('name', 'title'); 182 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 183 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1'); 184 Auth::login($user); 185 186 $record = $tree->createMediaObject("0 @@ OBJE\n1 FILE foo.jpeg"); 187 self::assertTrue($record->isPendingAddition()); 188 189 $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, '1'); 190 $record = $tree->createMediaObject("0 @@ OBJE\n1 FILE foo.jpeg"); 191 self::assertFalse($record->isPendingAddition()); 192 } 193 194 public function testCreateInvalidRecord(): void 195 { 196 $this->expectException(InvalidArgumentException::class); 197 198 $user_service = new UserService(); 199 $gedcom_import_service = new GedcomImportService(); 200 $tree_service = new TreeService($gedcom_import_service); 201 $tree = $tree_service->create('name', 'title'); 202 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 203 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1'); 204 Auth::login($user); 205 206 $tree->createRecord("0 @@FOO\n1 NOTE noted"); 207 } 208 209 public function testCreateRecord(): void 210 { 211 $user_service = new UserService(); 212 $gedcom_import_service = new GedcomImportService(); 213 $tree_service = new TreeService($gedcom_import_service); 214 $tree = $tree_service->create('name', 'title'); 215 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 216 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1'); 217 Auth::login($user); 218 219 $record = $tree->createRecord("0 @@ FOO\n1 NOTE noted"); 220 self::assertTrue($record->isPendingAddition()); 221 222 $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, '1'); 223 $record = $tree->createRecord("0 @@ FOO\n1 NOTE noted"); 224 self::assertFalse($record->isPendingAddition()); 225 } 226 227 public function testSignificantIndividual(): void 228 { 229 $gedcom_import_service = new GedcomImportService(); 230 $user_service = new UserService(); 231 $tree_service = new TreeService($gedcom_import_service); 232 $tree = $tree_service->create('name', 'title'); 233 $user = $user_service->create('user', 'User', 'user@example.com', 'secret'); 234 $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, '1'); 235 Auth::login($user); 236 237 // Delete the tree's default individual. 238 $gedcom_import_service->updateRecord('0 @X1@ INDI', $tree, true); 239 240 // No individuals in tree? Fake individual 241 self::assertSame('I', $tree->significantIndividual($user)->xref()); 242 243 $record1 = $tree->createIndividual("0 @@ INDI\n1 SEX F\n1 NAME Foo /Bar/"); 244 $record2 = $tree->createIndividual("0 @@ INDI\n1 SEX F\n1 NAME Foo /Bar/"); 245 $record3 = $tree->createIndividual("0 @@ INDI\n1 SEX F\n1 NAME Foo /Bar/"); 246 $record4 = $tree->createIndividual("0 @@ INDI\n1 SEX F\n1 NAME Foo /Bar/"); 247 248 // Individuals exist? First one (lowest XREF). 249 self::assertSame($record1->xref(), $tree->significantIndividual($user)->xref()); 250 251 // Preference for tree? 252 $tree->setPreference('PEDIGREE_ROOT_ID', $record2->xref()); 253 self::assertSame($record2->xref(), $tree->significantIndividual($user)->xref()); 254 255 // User preference 256 $tree->setUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF, $record3->xref()); 257 self::assertSame($record3->xref(), $tree->significantIndividual($user)->xref()); 258 259 // User record 260 $tree->setUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF, $record4->xref()); 261 self::assertSame($record4->xref(), $tree->significantIndividual($user)->xref()); 262 } 263 264 public function testImportAndDeleteGedcomFile(): void 265 { 266 $gedcom_import_service = new GedcomImportService(); 267 $tree_service = new TreeService($gedcom_import_service); 268 $tree = $this->importTree('demo.ged'); 269 self::assertNotNull($tree_service->all()->get('demo.ged')); 270 Site::setPreference('DEFAULT_GEDCOM', $tree->name()); 271 272 $tree_service->delete($tree); 273 274 self::assertNull($tree_service->all()->get('demo.ged')); 275 self::assertSame('', Site::getPreference('DEFAULT_GEDCOM')); 276 } 277 278 public function testHasPendingEdits(): void 279 { 280 $user_service = new UserService(); 281 $tree = $this->importTree('demo.ged'); 282 $user = $user_service->create('admin', 'Administrator', 'admin@example.com', 'secret'); 283 $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, '1'); 284 Auth::login($user); 285 286 $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, '1'); 287 $tree->createIndividual("0 @@ INDI\n1 SEX F\n1 NAME Foo /Bar/"); 288 self::assertFalse($tree->hasPendingEdit()); 289 290 $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, ''); 291 $tree->createIndividual("0 @@ INDI\n1 SEX F\n1 NAME Foo /Bar/"); 292 self::assertTrue($tree->hasPendingEdit()); 293 } 294 295 public function testExportGedcom(): void 296 { 297 $tree = $this->importTree('demo.ged'); 298 299 $gedcom_export_service = new GedcomExportService(new Psr17Factory(), new Psr17Factory()); 300 301 $resource = $gedcom_export_service->export($tree, true); 302 $original = file_get_contents(__DIR__ . '/../data/demo.ged'); 303 $export = stream_get_contents($resource); 304 fclose($resource); 305 306 // The version, date and time in the HEAD record will be different. 307 $original = preg_replace('/\n2 VERS .*/', '', $original, 1); 308 $export = preg_replace('/\n2 VERS .*/', '', $export, 1); 309 $original = preg_replace('/\n1 DATE .. ... ..../', '', $original, 1); 310 $export = preg_replace('/\n1 DATE .. ... ..../', '', $export, 1); 311 $original = preg_replace('/\n2 TIME ..:..:../', '', $original, 1); 312 $export = preg_replace('/\n2 TIME ..:..:../', '', $export, 1); 313 314 self::assertSame($original, $export); 315 } 316} 317