[ 'scope' => [new WP_Term(['slug' => 'global', 'name' => 'global'])], 'layout_type' => [new WP_Term(['slug' => 'section', 'name' => 'section'])], 'layout_category' => [new WP_Term(['slug' => 'kopf', 'name' => 'Kopfbereiche'])], ], 1055 => [ 'scope' => [new WP_Term(['slug' => 'non_global', 'name' => 'non_global'])], 'layout_type' => [new WP_Term(['slug' => 'section', 'name' => 'section'])], ], 1057 => [], ]; Functions\when('get_the_terms')->alias( static fn (int $id, string $taxonomy): array|false => $terms[$id][$taxonomy] ?? false ); Functions\when('get_post_meta')->alias( static fn (int $id, string $key): string => $id === 1057 && $key === '_et_pb_module_type' ? 'divi/blurb' : '' ); } public function testGlobalScopeAndLayoutTypeFromTheTerms(): void { $store = new DiviLibraryStore(); self::assertTrue($store->isGlobal(1058)); self::assertFalse($store->isGlobal(1055)); self::assertSame('section', $store->layoutType(1058)); self::assertNull($store->layoutType(1057)); self::assertSame(['Kopfbereiche'], $store->termNames(1058, 'layout_category')); } public function testDescribesAnEntry(): void { $post = new WP_Post([ 'ID' => 1057, 'post_type' => 'et_pb_layout', 'post_title' => '[Fixture] Library-Modul', 'post_modified_gmt' => '2026-09-26 20:56:29', ]); self::assertSame( [ 'id' => 1057, 'title' => '[Fixture] Library-Modul', 'layout_type' => null, 'scope' => 'normal', 'module_type' => 'divi/blurb', 'categories' => [], 'tags' => [], 'modified' => '2026-09-26 20:56:29', ], (new DiviLibraryStore())->describe($post) ); } public function testFindQueriesPublishedEntriesWithFilters(): void { $captured = []; Functions\when('get_posts')->alias(static function (array $args) use (&$captured): array { $captured = $args; return []; }); (new DiviLibraryStore())->find( ['layout_type' => 'section', 'scope' => 'normal', 'search' => 'Kopf', 'category' => 'Kopfbereiche'], 20, 40 ); self::assertSame('et_pb_layout', $captured['post_type']); self::assertSame('publish', $captured['post_status']); self::assertSame(21, $captured['posts_per_page']); self::assertSame(40, $captured['offset']); self::assertSame('Kopf', $captured['s']); self::assertSame(['post_title'], $captured['search_columns'], 'audit L3: the title only'); self::assertArrayNotHasKey('author', $captured); self::assertSame( [ 'relation' => 'AND', ['taxonomy' => 'layout_type', 'field' => 'slug', 'terms' => ['section']], ['taxonomy' => 'scope', 'field' => 'slug', 'terms' => ['global'], 'operator' => 'NOT IN'], ['taxonomy' => 'layout_category', 'field' => 'name', 'terms' => ['Kopfbereiche']], ], $captured['tax_query'] ); } /** Audit L3: users without edit_others_posts query their own entries only. */ public function testFindRestrictsToAnAuthor(): void { $captured = []; Functions\when('get_posts')->alias(static function (array $args) use (&$captured): array { $captured = $args; return []; }); (new DiviLibraryStore())->find(['author' => 12], 20, 0); self::assertSame(12, $captured['author']); } public function testReportsMissingTaxonomies(): void { Functions\when('taxonomy_exists')->alias(static fn (string $taxonomy): bool => $taxonomy !== 'scope'); self::assertSame(['scope'], (new DiviLibraryStore())->missingTaxonomies()); } private static function draft(?FakeChangeLog $log = null): NewDraft { return new NewDraft(null, $log ?? FakeChangeLog::forbidden(), 'lit/library-save', 'et_pb_layout', 'Bibliothekseintrag'); } public function testCreatesAPublishedEntryWithTermsAndMeta(): void { Functions\when('taxonomy_exists')->justReturn(true); $inserted = []; Functions\when('wp_insert_post')->alias(static function (array $data) use (&$inserted): int { $inserted = $data; return 2001; }); $terms = []; Functions\when('wp_set_object_terms')->alias( static function (int $id, array $values, string $taxonomy) use (&$terms): array { $terms[$taxonomy] = $values; return [1]; } ); $meta = []; Functions\when('update_post_meta')->alias(static function (int $id, string $key, string $value) use (&$meta): bool { $meta[$key] = $value; return true; }); // By reference: an arrow function would capture the still empty $meta. Functions\when('get_post_meta')->alias(static function (int $id, string $key) use (&$meta): string { return $meta[$key] ?? ''; }); $spec = new LibraryItemSpec('Öffnungszeiten', 'module', true, 'regular', 'divi/text', 'page'); self::assertSame(2001, (new DiviLibraryStore())->create($spec, self::draft())); self::assertSame( ['post_type' => 'et_pb_layout', 'post_status' => 'publish', 'post_title' => 'Öffnungszeiten', 'post_content' => ''], $inserted ); self::assertSame(['scope' => ['global'], 'layout_type' => ['module'], 'module_width' => ['regular']], $terms); self::assertSame('[]', $meta['_et_pb_excluded_global_options']); } public function testAFailedStepDeletesTheNewEntry(): void { Functions\when('taxonomy_exists')->justReturn(true); Functions\when('wp_insert_post')->justReturn(2001); Functions\when('wp_set_object_terms')->justReturn(new \WP_Error('invalid_taxonomy', 'Ungültige Taxonomie.')); Functions\expect('wp_delete_post')->once()->with(2001, true)->andReturn(new WP_Post(['ID' => 2001])); try { (new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft()); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertTrue($e->rolledBack); } } /** Ruling P6: a failed rollback is an honest partial write, logged as partial (NewDraft). */ public function testAFailedRollbackIsAPartialWrite(): void { Functions\when('taxonomy_exists')->justReturn(true); Functions\when('wp_insert_post')->justReturn(2001); Functions\when('wp_set_object_terms')->justReturn([1]); Functions\when('update_post_meta')->justReturn(false); Functions\when('get_post_meta')->justReturn(''); Functions\when('wp_delete_post')->justReturn(false); $log = FakeChangeLog::partialsOnly(); try { (new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft($log)); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertFalse($e->rolledBack); self::assertSame(2001, $e->postId); } $log->assertPartials([ 'ability' => 'lit/library-save', 'object_type' => 'et_pb_layout', 'object_id' => '2001', 'summary' => 'Bibliothekseintrag 2001: Anlegen nur teilweise zurückgenommen', ]); } /** Ruling P6: a throwing insert hook leaves the entry's existence unknown – partial write without id. */ public function testAThrowingInsertHookIsAPartialWrite(): void { Functions\when('taxonomy_exists')->justReturn(true); Functions\when('wp_insert_post')->alias(static function (): never { throw new \RuntimeException('hook kaputt'); }); $log = FakeChangeLog::partialsOnly(); try { (new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft($log)); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertFalse($e->rolledBack); self::assertNull($e->postId); } self::assertCount(1, $log->partials); } public function testARefusedInsertCreatesNothing(): void { Functions\when('taxonomy_exists')->justReturn(true); Functions\when('wp_insert_post')->justReturn(new \WP_Error('db_insert_error', 'Datenbankfehler.')); Functions\expect('wp_delete_post')->never(); try { (new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft()); self::fail('WriteFailed expected'); } catch (WriteFailed $e) { self::assertTrue($e->rolledBack); } } public function testRefusesWhenTheLibraryTaxonomiesAreMissing(): void { Functions\when('taxonomy_exists')->justReturn(false); Functions\expect('wp_insert_post')->never(); $this->expectException(WriteFailed::class); (new DiviLibraryStore())->create(new LibraryItemSpec('X', 'section', false, 'regular', null, 'page'), self::draft()); } }