the browser-facing portion of osu!
at master 308 lines 9.2 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6namespace Tests\Libraries\Transactions; 7 8use App\Exceptions\ModelNotSavedException; 9use App\Libraries\Transactions\AfterCommit; 10use App\Libraries\Transactions\AfterRollback; 11use App\Libraries\TransactionStateManager; 12use App\Models\Model; 13use DB; 14use Exception; 15use Illuminate\Support\Facades\Schema; 16use Tests\TestCase; 17 18class AfterCommitTest extends TestCase 19{ 20 protected $connectionsToTransact = []; 21 22 private $exceptionMessage = 'it should not run afterCommit'; 23 24 public function testModelAfterCommitSupportDoesEnlist() 25 { 26 $model = $this->afterCommittable(); 27 $model->save(); 28 29 $this->assertSame(1, $model->afterCommitCount); 30 } 31 32 public function testModelWithoutAfterCommitSupportDoesNotEnlist() 33 { 34 $model = $this->notAfterCommittable(); 35 $model->save(); 36 37 $this->assertSame(0, $model->afterCommitCount); 38 } 39 40 public function testModelAfterCommitAfterTransaction() 41 { 42 DB::beginTransaction(); 43 DB::commit(); 44 45 $model = $this->afterCommittable(); 46 $model->save(); 47 48 $this->assertSame(1, $model->afterCommitCount); 49 } 50 51 public function testModelAfterCommitTransaction() 52 { 53 // count should increase after transaction completes but not before. 54 $model = $this->afterCommittable(); 55 56 DB::transaction(function () use ($model) { 57 $model->save(); 58 59 $this->assertSame(1, count($this->getPendingCommits('mysql'))); 60 $this->assertSame(0, $model->afterCommitCount); 61 }); 62 63 $this->assertSame(0, count($this->getPendingCommits('mysql'))); 64 $this->assertSame(1, $model->afterCommitCount); 65 } 66 67 public function testModelAfterCommitTransactionUnrelatedConnection() 68 { 69 // count should increase after save. 70 $model = $this->afterCommittable(); 71 72 DB::connection('mysql-store')->transaction(function () use ($model) { 73 $model->save(); 74 75 $this->assertFalse($this->getTransactionState('mysql')->isReal()); 76 $this->assertCount(0, $this->getPendingCommits('')); 77 $this->assertCount(0, $this->getPendingCommits('mysql-store')); 78 $this->assertSame(1, $model->afterCommitCount); 79 }); 80 81 $this->assertSame(1, $model->afterCommitCount); 82 } 83 84 public function testMultipleSaveInTransaction() 85 { 86 $model = $this->afterCommittable(); 87 88 DB::transaction(function () use ($model) { 89 $model->save(); 90 $model->save(); 91 92 $this->assertSame(0, $model->afterCommitCount); 93 $this->assertSame(2, count($this->getPendingCommits('mysql'))); 94 $this->assertSame(1, count($this->getPendingUniqueCommits('mysql'))); 95 }); 96 97 $this->assertSame(0, count($this->getPendingCommits('mysql'))); 98 $this->assertSame(1, $model->afterCommitCount); 99 } 100 101 public function testNestedTransactions() 102 { 103 $model = $this->afterCommittable(); 104 105 DB::transaction(function () use ($model) { 106 $model->save(); 107 108 DB::transaction(function () use ($model) { 109 $model->save(); 110 }); 111 112 $this->assertSame(0, $model->afterCommitCount); 113 $this->assertSame(2, count($this->getPendingCommits('mysql'))); 114 $this->assertSame(1, count($this->getPendingUniqueCommits('mysql'))); 115 }); 116 117 $this->assertSame(0, count($this->getPendingCommits('mysql'))); 118 $this->assertSame(1, $model->afterCommitCount); 119 } 120 121 public function testExceptionThrown() 122 { 123 $model = $this->afterCommittable(); 124 125 try { 126 DB::transaction(function () use ($model) { 127 $model->save(); 128 129 throw new Exception($this->exceptionMessage); 130 }); 131 } catch (Exception $e) { 132 $this->assertSame($this->exceptionMessage, $e->getMessage()); 133 } 134 135 $this->assertSame(0, count($this->getPendingCommits('mysql'))); 136 $this->assertSame(0, $model->afterCommitCount); 137 } 138 139 public function testExceptionThrownAfterAnotherTransaction() 140 { 141 $model = $this->afterCommittable(); 142 143 try { 144 DB::transaction(function () use ($model) { 145 $model->save(); 146 147 DB::transaction(function () use ($model) { 148 $model->save(); 149 }); 150 151 throw new Exception($this->exceptionMessage); 152 }); 153 } catch (Exception $e) { 154 $this->assertSame($this->exceptionMessage, $e->getMessage()); 155 } 156 157 $this->assertSame(0, count($this->getPendingCommits('mysql'))); 158 $this->assertSame(0, $model->afterCommitCount); 159 } 160 161 public function testExceptionThrownInOtherConnection() 162 { 163 $model = $this->afterCommittable(); 164 165 try { 166 // After commit only runs if all transactions in scope complete. 167 DB::connection('mysql-store')->transaction(function () use ($model) { 168 DB::transaction(function () use ($model) { 169 $model->save(); 170 }); 171 172 throw new Exception($this->exceptionMessage); 173 }); 174 } catch (Exception $e) { 175 $this->assertSame($this->exceptionMessage, $e->getMessage()); 176 } 177 178 $this->assertSame(0, $model->afterCommitCount); 179 } 180 181 public function testRollbackExplictlyCalled() 182 { 183 $model = $this->afterCommittable(); 184 185 // Explictly rolling back single transaction level should still allow 186 // after commit to run at the end. 187 // Same behaviour as Rails without enlisting a new transaction. 188 DB::transaction(function () use ($model) { 189 $model->save(); 190 191 $this->assertSame(0, $model->afterCommitCount); 192 DB::transaction(function () { 193 DB::rollBack(); // this breaks the tests at the end when running on Travis 194 }); 195 }); 196 197 $this->assertSame(1, $model->afterCommitCount); 198 } 199 200 public function testSaveOrExplode() 201 { 202 $model = $this->afterCommittable(); 203 $model->result = false; 204 $thrown = false; 205 206 try { 207 DB::transaction(function () use ($model) { 208 $model->saveOrExplode(); 209 }); 210 } catch (ModelNotSavedException $e) { 211 $thrown = true; 212 } 213 214 $this->assertTrue($thrown); 215 $this->assertSame(0, count($this->getPendingCommits('mysql'))); 216 $this->assertSame(0, $model->afterCommitCount); 217 $this->assertSame(1, $model->afterRollbackCount); 218 } 219 220 protected function setUp(): void 221 { 222 parent::setUp(); 223 224 // not ideal to create the table between every test 225 // but Laravel's resolvers do not work in the 226 // setUpBeforeClass/tearDownAfterClass methods. 227 228 // force cleanup 229 if (Schema::hasTable('test_after_commit')) { 230 Schema::drop('test_after_commit'); 231 } 232 233 // create a dummy table 234 Schema::create('test_after_commit', function ($table) { 235 $table->charset = 'utf8mb4'; 236 $table->increments('id'); 237 $table->timestamp('created_at')->nullable(); 238 $table->timestamp('updated_at')->nullable(); 239 }); 240 } 241 242 protected function tearDown(): void 243 { 244 Schema::drop('test_after_commit'); 245 246 parent::tearDown(); 247 } 248 249 private function getPendingCommits(string $connection) 250 { 251 $state = $this->getTransactionState($connection); 252 253 return $state ? $this->invokeProperty($state, 'commits') : null; 254 } 255 256 private function getPendingUniqueCommits(string $connection) 257 { 258 $state = $this->getTransactionState($connection); 259 260 return $state ? $this->invokeMethod($state, 'uniqueCommits') : null; 261 } 262 263 private function getTransactionState(string $connection) 264 { 265 return resolve(TransactionStateManager::class)->current($connection); 266 } 267 268 /* 269 Test double helpers. 270 */ 271 272 private function notAfterCommittable() 273 { 274 return new class extends Model { 275 public $afterCommitCount = 0; 276 277 protected $connection = 'mysql'; 278 protected $table = 'test_after_commit'; 279 }; 280 } 281 282 private function afterCommittable() 283 { 284 return new class extends Model implements AfterCommit, AfterRollback { 285 public $afterCommitCount = 0; 286 public $afterRollbackCount = 0; 287 public $result = true; 288 289 protected $connection = 'mysql'; 290 protected $table = 'test_after_commit'; 291 292 public function afterCommit() 293 { 294 $this->afterCommitCount++; 295 } 296 297 public function afterRollback() 298 { 299 $this->afterRollbackCount++; 300 } 301 302 public function save(array $options = []) 303 { 304 return parent::save($options) && $this->result; 305 } 306 }; 307 } 308}