1<?php
2
3namespace MediaWiki\Tests\Storage;
4
5use Content;
6use MediaWiki\Revision\MutableRevisionSlots;
7use MediaWiki\Revision\RevisionAccessException;
8use MediaWiki\Revision\RevisionSlots;
9use MediaWiki\Revision\SlotRecord;
10use MediaWiki\Storage\RevisionSlotsUpdate;
11use MediaWikiIntegrationTestCase;
12use WikitextContent;
13
14/**
15 * @covers \MediaWiki\Storage\RevisionSlotsUpdate
16 */
17class RevisionSlotsUpdateTest extends MediaWikiIntegrationTestCase {
18
19	public function provideNewFromRevisionSlots() {
20		$slotA = SlotRecord::newUnsaved( 'A', new WikitextContent( 'A' ) );
21		$slotB = SlotRecord::newUnsaved( 'B', new WikitextContent( 'B' ) );
22		$slotC = SlotRecord::newUnsaved( 'C', new WikitextContent( 'C' ) );
23
24		$slotB2 = SlotRecord::newUnsaved( 'B', new WikitextContent( 'B2' ) );
25
26		$parentSlots = new RevisionSlots( [
27			'A' => $slotA,
28			'B' => $slotB,
29			'C' => $slotC,
30		] );
31
32		$newSlots = new RevisionSlots( [
33			'A' => $slotA,
34			'B' => $slotB2,
35		] );
36
37		yield [ $newSlots, null, [ 'A', 'B' ], [] ];
38		yield [ $newSlots, $parentSlots, [ 'B' ], [ 'C' ] ];
39	}
40
41	/**
42	 * @dataProvider provideNewFromRevisionSlots
43	 */
44	public function testNewFromRevisionSlots(
45		RevisionSlots $newSlots,
46		?RevisionSlots $parentSlots,
47		array $modified,
48		array $removed
49	) {
50		$update = RevisionSlotsUpdate::newFromRevisionSlots( $newSlots, $parentSlots );
51
52		$this->assertEquals( $modified, $update->getModifiedRoles() );
53		$this->assertEquals( $removed, $update->getRemovedRoles() );
54
55		foreach ( $modified as $role ) {
56			$this->assertSame( $newSlots->getSlot( $role ), $update->getModifiedSlot( $role ) );
57		}
58	}
59
60	public function provideNewFromContent() {
61		$slotA = SlotRecord::newUnsaved( 'A', new WikitextContent( 'A' ) );
62		$slotB = SlotRecord::newUnsaved( 'B', new WikitextContent( 'B' ) );
63		$slotC = SlotRecord::newUnsaved( 'C', new WikitextContent( 'C' ) );
64
65		$parentSlots = new RevisionSlots( [
66			'A' => $slotA,
67			'B' => $slotB,
68			'C' => $slotC,
69		] );
70
71		$newContent = [
72			'A' => new WikitextContent( 'A' ),
73			'B' => new WikitextContent( 'B2' ),
74		];
75
76		yield [ $newContent, null, [ 'A', 'B' ] ];
77		yield [ $newContent, $parentSlots, [ 'B' ] ];
78	}
79
80	/**
81	 * @dataProvider provideNewFromContent
82	 */
83	public function testNewFromContent(
84		array $newContent,
85		RevisionSlots $parentSlots = null,
86		array $modified = []
87	) {
88		$update = RevisionSlotsUpdate::newFromContent( $newContent, $parentSlots );
89
90		$this->assertEquals( $modified, $update->getModifiedRoles() );
91		$this->assertSame( [], $update->getRemovedRoles() );
92	}
93
94	public function testConstructor() {
95		$update = new RevisionSlotsUpdate();
96
97		$this->assertSame( [], $update->getModifiedRoles() );
98		$this->assertSame( [], $update->getRemovedRoles() );
99
100		$slotA = SlotRecord::newUnsaved( 'A', new WikitextContent( 'A' ) );
101		$update = new RevisionSlotsUpdate( [ 'A' => $slotA ] );
102
103		$this->assertEquals( [ 'A' ], $update->getModifiedRoles() );
104		$this->assertSame( [], $update->getRemovedRoles() );
105
106		$update = new RevisionSlotsUpdate( [ 'A' => $slotA ], [ 'X' ] );
107
108		$this->assertEquals( [ 'A' ], $update->getModifiedRoles() );
109		$this->assertEquals( [ 'X' ], $update->getRemovedRoles() );
110	}
111
112	public function testModifySlot() {
113		$slots = new RevisionSlotsUpdate();
114
115		$this->assertSame( [], $slots->getModifiedRoles() );
116		$this->assertSame( [], $slots->getRemovedRoles() );
117
118		$slotA = SlotRecord::newUnsaved( 'some', new WikitextContent( 'A' ) );
119		$slots->modifySlot( $slotA );
120		$this->assertTrue( $slots->isModifiedSlot( 'some' ) );
121		$this->assertFalse( $slots->isRemovedSlot( 'some' ) );
122		$this->assertSame( $slotA, $slots->getModifiedSlot( 'some' ) );
123		$this->assertSame( [ 'some' ], $slots->getModifiedRoles() );
124		$this->assertSame( [], $slots->getRemovedRoles() );
125
126		$slotB = SlotRecord::newUnsaved( 'other', new WikitextContent( 'B' ) );
127		$slots->modifySlot( $slotB );
128		$this->assertTrue( $slots->isModifiedSlot( 'other' ) );
129		$this->assertFalse( $slots->isRemovedSlot( 'other' ) );
130		$this->assertSame( $slotB, $slots->getModifiedSlot( 'other' ) );
131		$this->assertSame( [ 'some', 'other' ], $slots->getModifiedRoles() );
132		$this->assertSame( [], $slots->getRemovedRoles() );
133
134		// modify slot A again
135		$slots->modifySlot( $slotA );
136		$this->assertArrayEquals( [ 'some', 'other' ], $slots->getModifiedRoles() );
137
138		// remove modified slot
139		$slots->removeSlot( 'some' );
140		$this->assertSame( [ 'other' ], $slots->getModifiedRoles() );
141		$this->assertSame( [ 'some' ], $slots->getRemovedRoles() );
142
143		// modify removed slot
144		$slots->modifySlot( $slotA );
145		$this->assertArrayEquals( [ 'some', 'other' ], $slots->getModifiedRoles() );
146		$this->assertSame( [], $slots->getRemovedRoles() );
147	}
148
149	public function testRemoveSlot() {
150		$slots = new RevisionSlotsUpdate();
151
152		$slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
153		$slots->modifySlot( $slotA );
154
155		$this->assertSame( [ 'main' ], $slots->getModifiedRoles() );
156
157		$slots->removeSlot( SlotRecord::MAIN );
158		$slots->removeSlot( 'other' );
159		$this->assertSame( [], $slots->getModifiedRoles() );
160		$this->assertSame( [ 'main', 'other' ], $slots->getRemovedRoles() );
161		$this->assertTrue( $slots->isRemovedSlot( SlotRecord::MAIN ) );
162		$this->assertTrue( $slots->isRemovedSlot( 'other' ) );
163		$this->assertFalse( $slots->isModifiedSlot( SlotRecord::MAIN ) );
164
165		// removing the same slot again should not trigger an error
166		$slots->removeSlot( SlotRecord::MAIN );
167
168		// getting a slot after removing it should fail
169		$this->expectException( RevisionAccessException::class );
170		$slots->getModifiedSlot( SlotRecord::MAIN );
171	}
172
173	public function testGetModifiedRoles() {
174		$slots = new RevisionSlotsUpdate( [], [ 'xyz' ] );
175
176		$this->assertSame( [], $slots->getModifiedRoles() );
177
178		$slots->modifyContent( SlotRecord::MAIN, new WikitextContent( 'A' ) );
179		$slots->modifyContent( 'foo', new WikitextContent( 'Foo' ) );
180		$this->assertSame( [ 'main', 'foo' ], $slots->getModifiedRoles() );
181
182		$slots->removeSlot( SlotRecord::MAIN );
183		$this->assertSame( [ 'foo' ], $slots->getModifiedRoles() );
184	}
185
186	public function testGetRemovedRoles() {
187		$slotA = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
188		$slots = new RevisionSlotsUpdate( [ $slotA ] );
189
190		$this->assertSame( [], $slots->getRemovedRoles() );
191
192		$slots->removeSlot( SlotRecord::MAIN, new WikitextContent( 'A' ) );
193		$slots->removeSlot( 'foo', new WikitextContent( 'Foo' ) );
194
195		$this->assertSame( [ 'main', 'foo' ], $slots->getRemovedRoles() );
196
197		$slots->modifyContent( SlotRecord::MAIN, new WikitextContent( 'A' ) );
198		$this->assertSame( [ 'foo' ], $slots->getRemovedRoles() );
199	}
200
201	public function provideHasSameUpdates() {
202		$fooX = SlotRecord::newUnsaved( 'x', new WikitextContent( 'Foo' ) );
203		$barZ = SlotRecord::newUnsaved( 'z', new WikitextContent( 'Bar' ) );
204
205		$a = new RevisionSlotsUpdate();
206		$a->modifySlot( $fooX );
207		$a->modifySlot( $barZ );
208		$a->removeSlot( 'Q' );
209
210		$a2 = new RevisionSlotsUpdate();
211		$a2->modifySlot( $fooX );
212		$a2->modifySlot( $barZ );
213		$a2->removeSlot( 'Q' );
214
215		$b = new RevisionSlotsUpdate();
216		$b->modifySlot( $barZ );
217		$b->removeSlot( 'Q' );
218
219		$c = new RevisionSlotsUpdate();
220		$c->modifySlot( $fooX );
221		$c->modifySlot( $barZ );
222
223		yield 'same instance' => [ $a, $a, true ];
224		yield 'same udpates' => [ $a, $a2, true ];
225
226		yield 'different modified' => [ $a, $b, false ];
227		yield 'different removed' => [ $a, $c, false ];
228	}
229
230	/**
231	 * @dataProvider provideHasSameUpdates
232	 */
233	public function testHasSameUpdates( RevisionSlotsUpdate $a, RevisionSlotsUpdate $b, $same ) {
234		$this->assertSame( $same, $a->hasSameUpdates( $b ) );
235		$this->assertSame( $same, $b->hasSameUpdates( $a ) );
236	}
237
238	/**
239	 * @param string $role
240	 * @param Content $content
241	 * @return SlotRecord
242	 */
243	private function newSavedSlot( $role, Content $content ) {
244		return SlotRecord::newSaved( 7, 7, 'xyz', SlotRecord::newUnsaved( $role, $content ) );
245	}
246
247	public function testApplyUpdate() {
248		/** @var SlotRecord[] $parentSlots */
249		$parentSlots = [
250			'X' => $this->newSavedSlot( 'X', new WikitextContent( 'X' ) ),
251			'Y' => $this->newSavedSlot( 'Y', new WikitextContent( 'Y' ) ),
252			'Z' => $this->newSavedSlot( 'Z', new WikitextContent( 'Z' ) ),
253		];
254		$slots = MutableRevisionSlots::newFromParentRevisionSlots( $parentSlots );
255		$update = RevisionSlotsUpdate::newFromContent( [
256			'A' => new WikitextContent( 'A' ),
257			'Y' => new WikitextContent( 'yyy' ),
258		] );
259
260		$update->removeSlot( 'Z' );
261
262		$update->apply( $slots );
263		$this->assertSame( [ 'X', 'Y', 'A' ], $slots->getSlotRoles() );
264		$this->assertSame( $update->getModifiedSlot( 'A' ), $slots->getSlot( 'A' ) );
265		$this->assertSame( $update->getModifiedSlot( 'Y' ), $slots->getSlot( 'Y' ) );
266	}
267
268}
269