TwoFAccountControllerTest.php 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  1. <?php
  2. namespace Tests\Api\v1\Controllers;
  3. use App\Api\v1\Controllers\TwoFAccountController;
  4. use App\Api\v1\Resources\TwoFAccountCollection;
  5. use App\Api\v1\Resources\TwoFAccountExportCollection;
  6. use App\Api\v1\Resources\TwoFAccountExportResource;
  7. use App\Api\v1\Resources\TwoFAccountReadResource;
  8. use App\Api\v1\Resources\TwoFAccountStoreResource;
  9. use App\Facades\Settings;
  10. use App\Models\Group;
  11. use App\Models\TwoFAccount;
  12. use App\Models\User;
  13. use App\Policies\TwoFAccountPolicy;
  14. use App\Providers\MigrationServiceProvider;
  15. use App\Providers\TwoFAuthServiceProvider;
  16. use Illuminate\Support\Facades\DB;
  17. use Illuminate\Support\Facades\Storage;
  18. use PHPUnit\Framework\Attributes\CoversClass;
  19. use PHPUnit\Framework\Attributes\DataProvider;
  20. use PHPUnit\Framework\Attributes\Test;
  21. use Tests\Classes\LocalFile;
  22. use Tests\Data\MigrationTestData;
  23. use Tests\Data\OtpTestData;
  24. use Tests\FeatureTestCase;
  25. /**
  26. * TwoFAccountControllerTest test class
  27. */
  28. #[CoversClass(TwoFAccountController::class)]
  29. #[CoversClass(TwoFAccountCollection::class)]
  30. #[CoversClass(TwoFAccountReadResource::class)]
  31. #[CoversClass(TwoFAccountStoreResource::class)]
  32. #[CoversClass(TwoFAccountExportResource::class)]
  33. #[CoversClass(TwoFAccountExportCollection::class)]
  34. #[CoversClass(MigrationServiceProvider::class)]
  35. #[CoversClass(TwoFAuthServiceProvider::class)]
  36. #[CoversClass(TwoFAccountPolicy::class)]
  37. class TwoFAccountControllerTest extends FeatureTestCase
  38. {
  39. /**
  40. * @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
  41. */
  42. protected $user;
  43. protected $anotherUser;
  44. /**
  45. * @var App\Models\Group
  46. */
  47. protected $userGroupA;
  48. protected $userGroupB;
  49. protected $anotherUserGroupA;
  50. protected $anotherUserGroupB;
  51. /**
  52. * @var App\Models\TwoFAccount
  53. */
  54. protected $twofaccountA;
  55. protected $twofaccountB;
  56. protected $twofaccountC;
  57. protected $twofaccountD;
  58. private const VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET = [
  59. 'id',
  60. 'group_id',
  61. 'service',
  62. 'account',
  63. 'icon',
  64. 'otp_type',
  65. 'digits',
  66. 'algorithm',
  67. 'period',
  68. 'counter',
  69. ];
  70. private const VALID_RESOURCE_STRUCTURE_WITH_SECRET = [
  71. 'id',
  72. 'group_id',
  73. 'service',
  74. 'account',
  75. 'icon',
  76. 'otp_type',
  77. 'secret',
  78. 'digits',
  79. 'algorithm',
  80. 'period',
  81. 'counter',
  82. ];
  83. private const VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP = [
  84. 'generated_at',
  85. 'otp_type',
  86. 'password',
  87. 'period',
  88. ];
  89. private const VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP = [
  90. 'otp_type',
  91. 'password',
  92. 'counter',
  93. ];
  94. private const VALID_EXPORT_STRUTURE = [
  95. 'app',
  96. 'schema',
  97. 'datetime',
  98. 'data' => [
  99. '*' => [
  100. 'otp_type',
  101. 'account',
  102. 'service',
  103. 'icon',
  104. 'icon_mime',
  105. 'icon_file',
  106. 'secret',
  107. 'digits',
  108. 'algorithm',
  109. 'period',
  110. 'counter',
  111. 'legacy_uri',
  112. ], ],
  113. ];
  114. private const JSON_FRAGMENTS_FOR_CUSTOM_TOTP = [
  115. 'service' => OtpTestData::SERVICE,
  116. 'account' => OtpTestData::ACCOUNT,
  117. 'otp_type' => 'totp',
  118. 'secret' => OtpTestData::SECRET,
  119. 'digits' => OtpTestData::DIGITS_CUSTOM,
  120. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  121. 'period' => OtpTestData::PERIOD_CUSTOM,
  122. 'counter' => null,
  123. ];
  124. private const JSON_FRAGMENTS_FOR_DEFAULT_TOTP = [
  125. 'service' => null,
  126. 'account' => OtpTestData::ACCOUNT,
  127. 'otp_type' => 'totp',
  128. 'secret' => OtpTestData::SECRET,
  129. 'digits' => OtpTestData::DIGITS_DEFAULT,
  130. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  131. 'period' => OtpTestData::PERIOD_DEFAULT,
  132. 'counter' => null,
  133. ];
  134. private const JSON_FRAGMENTS_FOR_CUSTOM_HOTP = [
  135. 'service' => OtpTestData::SERVICE,
  136. 'account' => OtpTestData::ACCOUNT,
  137. 'otp_type' => 'hotp',
  138. 'secret' => OtpTestData::SECRET,
  139. 'digits' => OtpTestData::DIGITS_CUSTOM,
  140. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  141. 'period' => null,
  142. 'counter' => OtpTestData::COUNTER_CUSTOM,
  143. ];
  144. private const JSON_FRAGMENTS_FOR_DEFAULT_HOTP = [
  145. 'service' => null,
  146. 'account' => OtpTestData::ACCOUNT,
  147. 'otp_type' => 'hotp',
  148. 'secret' => OtpTestData::SECRET,
  149. 'digits' => OtpTestData::DIGITS_DEFAULT,
  150. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  151. 'period' => null,
  152. 'counter' => OtpTestData::COUNTER_DEFAULT,
  153. ];
  154. private const ARRAY_OF_INVALID_PARAMETERS = [
  155. 'account' => null,
  156. 'otp_type' => 'totp',
  157. 'secret' => OtpTestData::SECRET,
  158. ];
  159. public function setUp() : void
  160. {
  161. parent::setUp();
  162. $this->user = User::factory()->create();
  163. $this->userGroupA = Group::factory()->for($this->user)->create();
  164. $this->userGroupB = Group::factory()->for($this->user)->create();
  165. $this->twofaccountA = TwoFAccount::factory()->for($this->user)->create([
  166. 'group_id' => $this->userGroupA->id,
  167. ]);
  168. $this->twofaccountB = TwoFAccount::factory()->for($this->user)->create([
  169. 'group_id' => $this->userGroupA->id,
  170. ]);
  171. $this->anotherUser = User::factory()->create();
  172. $this->anotherUserGroupA = Group::factory()->for($this->anotherUser)->create();
  173. $this->anotherUserGroupB = Group::factory()->for($this->anotherUser)->create();
  174. $this->twofaccountC = TwoFAccount::factory()->for($this->anotherUser)->create([
  175. 'group_id' => $this->anotherUserGroupA->id,
  176. ]);
  177. $this->twofaccountD = TwoFAccount::factory()->for($this->anotherUser)->create([
  178. 'group_id' => $this->anotherUserGroupB->id,
  179. ]);
  180. }
  181. #[Test]
  182. #[DataProvider('indexUrlParameterProvider')]
  183. public function test_index_returns_user_twofaccounts_only($urlParameter, $expected)
  184. {
  185. $response = $this->actingAs($this->user, 'api-guard')
  186. ->json('GET', '/api/v1/twofaccounts' . $urlParameter)
  187. ->assertOk()
  188. ->assertJsonCount(2, $key = null)
  189. ->assertJsonStructure([
  190. '*' => $expected,
  191. ])
  192. ->assertJsonFragment([
  193. 'id' => $this->twofaccountA->id,
  194. ])
  195. ->assertJsonFragment([
  196. 'id' => $this->twofaccountB->id,
  197. ])
  198. ->assertJsonMissing([
  199. 'id' => $this->twofaccountC->id,
  200. ])
  201. ->assertJsonMissing([
  202. 'id' => $this->twofaccountD->id,
  203. ]);
  204. }
  205. /**
  206. * Provide data for index tests
  207. */
  208. public static function indexUrlParameterProvider()
  209. {
  210. return [
  211. 'VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET' => [
  212. '',
  213. self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET,
  214. ],
  215. 'VALID_RESOURCE_STRUCTURE_WITH_SECRET' => [
  216. '?withSecret=1',
  217. self::VALID_RESOURCE_STRUCTURE_WITH_SECRET,
  218. ],
  219. ];
  220. }
  221. #[Test]
  222. public function test_show_returns_twofaccount_resource_with_secret()
  223. {
  224. $response = $this->actingAs($this->user, 'api-guard')
  225. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountA->id)
  226. ->assertOk()
  227. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_SECRET);
  228. }
  229. #[Test]
  230. public function test_show_returns_twofaccount_resource_without_secret()
  231. {
  232. $response = $this->actingAs($this->user, 'api-guard')
  233. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountA->id . '?withSecret=0')
  234. ->assertOk()
  235. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET);
  236. }
  237. //#[Test]
  238. // public function test_show_twofaccount_with_indeciphered_data_returns_replaced_data()
  239. // {
  240. // $dbEncryptionService = resolve('App\Services\DbEncryptionService');
  241. // $dbEncryptionService->setTo(true);
  242. // $twofaccount = TwoFAccount::factory()->create();
  243. // DB::table('twofaccounts')
  244. // ->where('id', $twofaccount->id)
  245. // ->update([
  246. // 'secret' => '**encrypted**',
  247. // 'account' => '**encrypted**',
  248. // ]);
  249. // $response = $this->actingAs($this->user, 'api-guard')
  250. // ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id)
  251. // ->assertJsonFragment([
  252. // 'secret' => '*indecipherable*',
  253. // 'account' => '*indecipherable*',
  254. // ]);
  255. // }
  256. #[Test]
  257. public function test_show_missing_twofaccount_returns_not_found()
  258. {
  259. $response = $this->actingAs($this->user, 'api-guard')
  260. ->json('GET', '/api/v1/twofaccounts/1000')
  261. ->assertNotFound()
  262. ->assertJsonStructure([
  263. 'message',
  264. ]);
  265. }
  266. #[Test]
  267. public function test_show_twofaccount_of_another_user_is_forbidden()
  268. {
  269. $response = $this->actingAs($this->user, 'api-guard')
  270. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountC->id)
  271. ->assertForbidden()
  272. ->assertJsonStructure([
  273. 'message',
  274. ]);
  275. }
  276. #[Test]
  277. #[DataProvider('accountCreationProvider')]
  278. public function test_store_without_encryption_returns_success_with_consistent_resource_structure($payload, $expected)
  279. {
  280. Settings::set('useEncryption', false);
  281. Storage::put('test.png', 'emptied to prevent missing resource replaced by null by the model getter');
  282. $response = $this->actingAs($this->user, 'api-guard')
  283. ->json('POST', '/api/v1/twofaccounts', $payload)
  284. ->assertCreated()
  285. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_SECRET)
  286. ->assertJsonFragment($expected);
  287. }
  288. #[Test]
  289. #[DataProvider('accountCreationProvider')]
  290. public function test_store_with_encryption_returns_success_with_consistent_resource_structure($payload, $expected)
  291. {
  292. Settings::set('useEncryption', true);
  293. Storage::put('test.png', 'emptied to prevent missing resource replaced by null by the model getter');
  294. $response = $this->actingAs($this->user, 'api-guard')
  295. ->json('POST', '/api/v1/twofaccounts', $payload)
  296. ->assertCreated()
  297. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_SECRET)
  298. ->assertJsonFragment($expected);
  299. }
  300. /**
  301. * Provide data for TwoFAccount store tests
  302. */
  303. public static function accountCreationProvider()
  304. {
  305. return [
  306. 'TOTP_FULL_CUSTOM_URI' => [
  307. [
  308. 'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
  309. ],
  310. self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP,
  311. ],
  312. 'TOTP_SHORT_URI' => [
  313. [
  314. 'uri' => OtpTestData::TOTP_SHORT_URI,
  315. ],
  316. self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP,
  317. ],
  318. 'ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP' => [
  319. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  320. self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP,
  321. ],
  322. 'ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP' => [
  323. OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP,
  324. self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP,
  325. ],
  326. 'HOTP_FULL_CUSTOM_URI' => [
  327. [
  328. 'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
  329. ],
  330. self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP,
  331. ],
  332. 'HOTP_SHORT_URI' => [
  333. [
  334. 'uri' => OtpTestData::HOTP_SHORT_URI,
  335. ],
  336. self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP,
  337. ],
  338. 'ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP' => [
  339. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP,
  340. self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP,
  341. ],
  342. 'ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP' => [
  343. OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP,
  344. self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP,
  345. ],
  346. ];
  347. }
  348. #[Test]
  349. public function test_store_with_invalid_uri_returns_validation_error()
  350. {
  351. $response = $this->actingAs($this->user, 'api-guard')
  352. ->json('POST', '/api/v1/twofaccounts', [
  353. 'uri' => OtpTestData::INVALID_OTPAUTH_URI,
  354. ])
  355. ->assertStatus(422);
  356. }
  357. #[Test]
  358. public function test_store_assigns_created_account_when_default_group_is_a_specific_one()
  359. {
  360. // Set the default group to a specific one
  361. $this->user['preferences->defaultGroup'] = $this->userGroupA->id;
  362. $this->user->save();
  363. $response = $this->actingAs($this->user, 'api-guard')
  364. ->json('POST', '/api/v1/twofaccounts', [
  365. 'uri' => OtpTestData::TOTP_SHORT_URI,
  366. ])
  367. ->assertJsonFragment([
  368. 'group_id' => $this->userGroupA->id,
  369. ]);
  370. }
  371. #[Test]
  372. public function test_store_assigns_created_account_when_default_group_is_the_active_one()
  373. {
  374. // Set the default group to be the active one
  375. $this->user['preferences->defaultGroup'] = -1;
  376. // Set the active group
  377. $this->user['preferences->activeGroup'] = $this->userGroupA->id;
  378. $this->user->save();
  379. $response = $this->actingAs($this->user, 'api-guard')
  380. ->json('POST', '/api/v1/twofaccounts', [
  381. 'uri' => OtpTestData::TOTP_SHORT_URI,
  382. ])
  383. ->assertJsonFragment([
  384. 'group_id' => $this->userGroupA->id,
  385. ]);
  386. }
  387. #[Test]
  388. public function test_store_assigns_created_account_when_default_group_is_no_group()
  389. {
  390. // Set the default group to No group
  391. $this->user['preferences->defaultGroup'] = 0;
  392. $this->user->save();
  393. $response = $this->actingAs($this->user, 'api-guard')
  394. ->json('POST', '/api/v1/twofaccounts', [
  395. 'uri' => OtpTestData::TOTP_SHORT_URI,
  396. ])
  397. ->assertJsonFragment([
  398. 'group_id' => null,
  399. ]);
  400. }
  401. #[Test]
  402. public function test_store_assigns_created_account_when_default_group_does_not_exist()
  403. {
  404. // Set the default group to a non-existing one
  405. $this->user['preferences->defaultGroup'] = 1000;
  406. $this->user->save();
  407. $response = $this->actingAs($this->user, 'api-guard')
  408. ->json('POST', '/api/v1/twofaccounts', [
  409. 'uri' => OtpTestData::TOTP_SHORT_URI,
  410. ])
  411. ->assertJsonFragment([
  412. 'group_id' => null,
  413. ]);
  414. }
  415. #[Test]
  416. public function test_update_totp_returns_success_with_updated_resource()
  417. {
  418. $response = $this->actingAs($this->user, 'api-guard')
  419. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  420. ->assertOk()
  421. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  422. }
  423. #[Test]
  424. public function test_update_hotp_returns_success_with_updated_resource()
  425. {
  426. $response = $this->actingAs($this->user, 'api-guard')
  427. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
  428. ->assertOk()
  429. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP);
  430. }
  431. #[Test]
  432. public function test_update_missing_twofaccount_returns_not_found()
  433. {
  434. $response = $this->actingAs($this->user, 'api-guard')
  435. ->json('PUT', '/api/v1/twofaccounts/1000', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  436. ->assertNotFound();
  437. }
  438. #[Test]
  439. public function test_update_twofaccount_with_invalid_data_returns_validation_error()
  440. {
  441. $twofaccount = TwoFAccount::factory()->create();
  442. $response = $this->actingAs($this->user, 'api-guard')
  443. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, self::ARRAY_OF_INVALID_PARAMETERS)
  444. ->assertStatus(422);
  445. }
  446. #[Test]
  447. public function test_update_twofaccount_of_another_user_is_forbidden()
  448. {
  449. $response = $this->actingAs($this->user, 'api-guard')
  450. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountC->id, OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
  451. ->assertForbidden()
  452. ->assertJsonStructure([
  453. 'message',
  454. ]);
  455. }
  456. #[Test]
  457. public function test_migrate_valid_gauth_payload_returns_success_with_consistent_resources()
  458. {
  459. $response = $this->actingAs($this->user, 'api-guard')
  460. ->json('POST', '/api/v1/twofaccounts/migration', [
  461. 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI,
  462. 'withSecret' => 1,
  463. ])
  464. ->assertOk()
  465. ->assertJsonCount(2, $key = null)
  466. ->assertJsonFragment([
  467. 'id' => 0,
  468. 'service' => OtpTestData::SERVICE,
  469. 'account' => OtpTestData::ACCOUNT,
  470. 'otp_type' => 'totp',
  471. 'secret' => OtpTestData::SECRET,
  472. 'digits' => OtpTestData::DIGITS_DEFAULT,
  473. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  474. 'period' => OtpTestData::PERIOD_DEFAULT,
  475. 'counter' => null,
  476. ])
  477. ->assertJsonFragment([
  478. 'id' => 0,
  479. 'service' => OtpTestData::SERVICE . '_bis',
  480. 'account' => OtpTestData::ACCOUNT . '_bis',
  481. 'otp_type' => 'totp',
  482. 'secret' => OtpTestData::SECRET,
  483. 'digits' => OtpTestData::DIGITS_DEFAULT,
  484. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  485. 'period' => OtpTestData::PERIOD_DEFAULT,
  486. 'counter' => null,
  487. ]);
  488. }
  489. #[Test]
  490. public function test_migrate_with_invalid_gauth_payload_returns_validation_error()
  491. {
  492. $response = $this->actingAs($this->user, 'api-guard')
  493. ->json('POST', '/api/v1/twofaccounts/migration', [
  494. 'uri' => MigrationTestData::INVALID_GOOGLE_AUTH_MIGRATION_URI,
  495. ])
  496. ->assertStatus(422);
  497. }
  498. #[Test]
  499. public function test_migrate_payload_with_duplicates_returns_negative_ids()
  500. {
  501. $twofaccount = TwoFAccount::factory()->for($this->user)->create([
  502. 'otp_type' => 'totp',
  503. 'account' => OtpTestData::ACCOUNT,
  504. 'service' => OtpTestData::SERVICE,
  505. 'secret' => OtpTestData::SECRET,
  506. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  507. 'digits' => OtpTestData::DIGITS_DEFAULT,
  508. 'period' => OtpTestData::PERIOD_DEFAULT,
  509. 'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
  510. 'icon' => '',
  511. ]);
  512. $response = $this->actingAs($this->user, 'api-guard')
  513. ->json('POST', '/api/v1/twofaccounts/migration?withSecret=1', [
  514. 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI,
  515. ])
  516. ->assertOk()
  517. ->assertJsonFragment([
  518. 'id' => -1,
  519. 'service' => OtpTestData::SERVICE,
  520. 'account' => OtpTestData::ACCOUNT,
  521. 'otp_type' => 'totp',
  522. 'secret' => OtpTestData::SECRET,
  523. 'digits' => OtpTestData::DIGITS_DEFAULT,
  524. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  525. 'period' => OtpTestData::PERIOD_DEFAULT,
  526. 'counter' => null,
  527. ])
  528. ->assertJsonFragment([
  529. 'id' => 0,
  530. 'service' => OtpTestData::SERVICE . '_bis',
  531. 'account' => OtpTestData::ACCOUNT . '_bis',
  532. 'otp_type' => 'totp',
  533. 'secret' => OtpTestData::SECRET,
  534. 'digits' => OtpTestData::DIGITS_DEFAULT,
  535. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  536. 'period' => OtpTestData::PERIOD_DEFAULT,
  537. 'counter' => null,
  538. ]);
  539. }
  540. #[Test]
  541. public function test_migrate_identify_duplicates_in_authenticated_user_twofaccounts_only()
  542. {
  543. $twofaccount = TwoFAccount::factory()->for($this->anotherUser)->create([
  544. 'otp_type' => 'totp',
  545. 'account' => OtpTestData::ACCOUNT,
  546. 'service' => OtpTestData::SERVICE,
  547. 'secret' => OtpTestData::SECRET,
  548. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  549. 'digits' => OtpTestData::DIGITS_DEFAULT,
  550. 'period' => OtpTestData::PERIOD_DEFAULT,
  551. 'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
  552. 'icon' => '',
  553. ]);
  554. $response = $this->actingAs($this->user, 'api-guard')
  555. ->json('POST', '/api/v1/twofaccounts/migration?withSecret=1', [
  556. 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI,
  557. ])
  558. ->assertOk()
  559. ->assertJsonFragment([
  560. 'id' => 0,
  561. 'account' => OtpTestData::ACCOUNT,
  562. 'service' => OtpTestData::SERVICE,
  563. 'otp_type' => 'totp',
  564. 'secret' => OtpTestData::SECRET,
  565. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  566. 'digits' => OtpTestData::DIGITS_DEFAULT,
  567. 'period' => OtpTestData::PERIOD_DEFAULT,
  568. 'icon' => null,
  569. ])
  570. ->assertJsonFragment([
  571. 'id' => 0,
  572. 'service' => OtpTestData::SERVICE . '_bis',
  573. 'account' => OtpTestData::ACCOUNT . '_bis',
  574. 'otp_type' => 'totp',
  575. 'secret' => OtpTestData::SECRET,
  576. 'digits' => OtpTestData::DIGITS_DEFAULT,
  577. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  578. 'period' => OtpTestData::PERIOD_DEFAULT,
  579. 'counter' => null,
  580. ]);
  581. }
  582. #[Test]
  583. public function test_migrate_invalid_gauth_payload_returns_bad_request()
  584. {
  585. $response = $this->actingAs($this->user, 'api-guard')
  586. ->json('POST', '/api/v1/twofaccounts/migration', [
  587. 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA,
  588. ])
  589. ->assertStatus(400)
  590. ->assertJsonStructure([
  591. 'message',
  592. ]);
  593. }
  594. #[Test]
  595. public function test_migrate_valid_aegis_json_file_returns_success()
  596. {
  597. $file = LocalFile::fake()->validAegisJsonFile();
  598. $response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
  599. ->actingAs($this->user, 'api-guard')
  600. ->json('POST', '/api/v1/twofaccounts/migration', [
  601. 'file' => $file,
  602. 'withSecret' => 1,
  603. ])
  604. ->assertOk()
  605. ->assertJsonCount(3, $key = null)
  606. ->assertJsonFragment([
  607. 'id' => 0,
  608. 'service' => OtpTestData::SERVICE,
  609. 'account' => OtpTestData::ACCOUNT,
  610. 'otp_type' => 'totp',
  611. 'secret' => OtpTestData::SECRET,
  612. 'digits' => OtpTestData::DIGITS_CUSTOM,
  613. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  614. 'period' => OtpTestData::PERIOD_CUSTOM,
  615. 'counter' => null,
  616. ])
  617. ->assertJsonFragment([
  618. 'id' => 0,
  619. 'service' => OtpTestData::SERVICE,
  620. 'account' => OtpTestData::ACCOUNT,
  621. 'otp_type' => 'hotp',
  622. 'secret' => OtpTestData::SECRET,
  623. 'digits' => OtpTestData::DIGITS_CUSTOM,
  624. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  625. 'period' => null,
  626. 'counter' => OtpTestData::COUNTER_CUSTOM,
  627. ])
  628. ->assertJsonFragment([
  629. 'id' => 0,
  630. 'service' => OtpTestData::STEAM,
  631. 'account' => OtpTestData::ACCOUNT,
  632. 'otp_type' => 'steamtotp',
  633. 'secret' => OtpTestData::STEAM_SECRET,
  634. 'digits' => OtpTestData::DIGITS_STEAM,
  635. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  636. 'period' => OtpTestData::PERIOD_DEFAULT,
  637. 'counter' => null,
  638. ]);
  639. }
  640. #[Test]
  641. #[DataProvider('invalidAegisJsonFileProvider')]
  642. public function test_migrate_invalid_aegis_json_file_returns_bad_request($file)
  643. {
  644. $response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
  645. ->actingAs($this->user, 'api-guard')
  646. ->json('POST', '/api/v1/twofaccounts/migration', [
  647. 'file' => $file,
  648. ])
  649. ->assertStatus(400);
  650. }
  651. /**
  652. * Provide invalid Aegis JSON files for import tests
  653. */
  654. public static function invalidAegisJsonFileProvider()
  655. {
  656. return [
  657. 'encryptedAegisJsonFile' => [
  658. LocalFile::fake()->encryptedAegisJsonFile(),
  659. ],
  660. 'invalidAegisJsonFile' => [
  661. LocalFile::fake()->invalidAegisJsonFile(),
  662. ],
  663. ];
  664. }
  665. #[Test]
  666. #[DataProvider('validPlainTextFileProvider')]
  667. public function test_migrate_valid_plain_text_file_returns_success($file)
  668. {
  669. $response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
  670. ->actingAs($this->user, 'api-guard')
  671. ->json('POST', '/api/v1/twofaccounts/migration', [
  672. 'file' => $file,
  673. 'withSecret' => 1,
  674. ])
  675. ->assertOk()
  676. ->assertJsonCount(3, $key = null)
  677. ->assertJsonFragment([
  678. 'id' => 0,
  679. 'service' => OtpTestData::SERVICE,
  680. 'account' => OtpTestData::ACCOUNT,
  681. 'otp_type' => 'totp',
  682. 'secret' => OtpTestData::SECRET,
  683. 'digits' => OtpTestData::DIGITS_CUSTOM,
  684. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  685. 'period' => OtpTestData::PERIOD_CUSTOM,
  686. 'counter' => null,
  687. ])
  688. ->assertJsonFragment([
  689. 'id' => 0,
  690. 'service' => OtpTestData::SERVICE,
  691. 'account' => OtpTestData::ACCOUNT,
  692. 'otp_type' => 'hotp',
  693. 'secret' => OtpTestData::SECRET,
  694. 'digits' => OtpTestData::DIGITS_CUSTOM,
  695. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  696. 'period' => null,
  697. 'counter' => OtpTestData::COUNTER_CUSTOM,
  698. ])
  699. ->assertJsonFragment([
  700. 'id' => 0,
  701. 'service' => OtpTestData::STEAM,
  702. 'account' => OtpTestData::ACCOUNT,
  703. 'otp_type' => 'steamtotp',
  704. 'secret' => OtpTestData::STEAM_SECRET,
  705. 'digits' => OtpTestData::DIGITS_STEAM,
  706. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  707. 'period' => OtpTestData::PERIOD_DEFAULT,
  708. 'counter' => null,
  709. ]);
  710. }
  711. /**
  712. * Provide valid Plain Text files for import tests
  713. */
  714. public static function validPlainTextFileProvider()
  715. {
  716. return [
  717. 'validPlainTextFile' => [
  718. LocalFile::fake()->validPlainTextFile(),
  719. ],
  720. 'validPlainTextFileWithNewLines' => [
  721. LocalFile::fake()->validPlainTextFileWithNewLines(),
  722. ],
  723. ];
  724. }
  725. #[Test]
  726. #[DataProvider('invalidPlainTextFileProvider')]
  727. public function test_migrate_invalid_plain_text_file_returns_bad_request($file)
  728. {
  729. $response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
  730. ->actingAs($this->user, 'api-guard')
  731. ->json('POST', '/api/v1/twofaccounts/migration', [
  732. 'file' => $file,
  733. ])
  734. ->assertStatus(400);
  735. }
  736. /**
  737. * Provide invalid Plain Text files for import tests
  738. */
  739. public static function invalidPlainTextFileProvider()
  740. {
  741. return [
  742. 'invalidPlainTextFileEmpty' => [
  743. LocalFile::fake()->invalidPlainTextFileEmpty(),
  744. ],
  745. 'invalidPlainTextFileNoUri' => [
  746. LocalFile::fake()->invalidPlainTextFileNoUri(),
  747. ],
  748. 'invalidPlainTextFileWithInvalidUri' => [
  749. LocalFile::fake()->invalidPlainTextFileWithInvalidUri(),
  750. ],
  751. 'invalidPlainTextFileWithInvalidLine' => [
  752. LocalFile::fake()->invalidPlainTextFileWithInvalidLine(),
  753. ],
  754. ];
  755. }
  756. #[Test]
  757. public function test_reorder_returns_success()
  758. {
  759. $response = $this->actingAs($this->user, 'api-guard')
  760. ->json('POST', '/api/v1/twofaccounts/reorder', [
  761. 'orderedIds' => [$this->twofaccountB->id, $this->twofaccountA->id],
  762. ])
  763. ->assertStatus(200)
  764. ->assertJsonStructure([
  765. 'message',
  766. ]);
  767. }
  768. #[Test]
  769. public function test_reorder_with_invalid_data_returns_validation_error()
  770. {
  771. $response = $this->actingAs($this->user, 'api-guard')
  772. ->json('POST', '/api/v1/twofaccounts/reorder', [
  773. 'orderedIds' => '3,2,1',
  774. ])
  775. ->assertStatus(422);
  776. }
  777. #[Test]
  778. public function test_reorder_twofaccounts_of_another_user_is_forbidden()
  779. {
  780. $response = $this->actingAs($this->user, 'api-guard')
  781. ->json('POST', '/api/v1/twofaccounts/reorder', [
  782. 'orderedIds' => [$this->twofaccountB->id, $this->twofaccountD->id],
  783. ])
  784. ->assertForbidden()
  785. ->assertJsonStructure([
  786. 'message',
  787. ]);
  788. }
  789. #[Test]
  790. public function test_preview_returns_success_with_resource()
  791. {
  792. $response = $this->actingAs($this->user, 'api-guard')
  793. ->json('POST', '/api/v1/twofaccounts/preview', [
  794. 'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
  795. ])
  796. ->assertOk()
  797. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  798. }
  799. #[Test]
  800. public function test_preview_with_invalid_data_returns_validation_error()
  801. {
  802. $response = $this->actingAs($this->user, 'api-guard')
  803. ->json('POST', '/api/v1/twofaccounts/preview', [
  804. 'uri' => OtpTestData::INVALID_OTPAUTH_URI,
  805. ])
  806. ->assertStatus(422);
  807. }
  808. #[Test]
  809. public function test_preview_with_unreachable_image_returns_success()
  810. {
  811. $response = $this->actingAs($this->user, 'api-guard')
  812. ->json('POST', '/api/v1/twofaccounts/preview', [
  813. 'uri' => OtpTestData::TOTP_URI_WITH_UNREACHABLE_IMAGE,
  814. ])
  815. ->assertOk()
  816. ->assertJsonFragment([
  817. 'icon' => null,
  818. ]);
  819. }
  820. #[Test]
  821. public function test_export_returns_json_migration_resource()
  822. {
  823. $this->twofaccountA = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  824. $this->twofaccountB = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
  825. $this->actingAs($this->user, 'api-guard')
  826. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountB->id)
  827. ->assertOk()
  828. ->assertJsonStructure(self::VALID_EXPORT_STRUTURE)
  829. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP)
  830. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
  831. }
  832. #[Test]
  833. public function test_export_too_many_ids_returns_bad_request()
  834. {
  835. TwoFAccount::factory()->count(102)->for($this->user)->create();
  836. $ids = DB::table('twofaccounts')->where('user_id', $this->user->id)->pluck('id')->implode(',');
  837. $response = $this->actingAs($this->user, 'api-guard')
  838. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $ids)
  839. ->assertStatus(400)
  840. ->assertJsonStructure([
  841. 'message',
  842. 'reason',
  843. ]);
  844. }
  845. #[Test]
  846. public function test_export_missing_twofaccount_returns_existing_ones_only()
  847. {
  848. $this->twofaccountA = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  849. $response = $this->actingAs($this->user, 'api-guard')
  850. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountA->id . ',1000')
  851. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  852. }
  853. #[Test]
  854. public function test_export_twofaccount_of_another_user_is_forbidden()
  855. {
  856. $response = $this->actingAs($this->user, 'api-guard')
  857. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountC->id)
  858. ->assertForbidden()
  859. ->assertJsonStructure([
  860. 'message',
  861. ]);
  862. }
  863. #[Test]
  864. public function test_get_otp_using_totp_twofaccount_id_returns_consistent_resource()
  865. {
  866. $twofaccount = TwoFAccount::factory()->for($this->user)->create([
  867. 'otp_type' => 'totp',
  868. 'account' => OtpTestData::ACCOUNT,
  869. 'service' => OtpTestData::SERVICE,
  870. 'secret' => OtpTestData::SECRET,
  871. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  872. 'digits' => OtpTestData::DIGITS_DEFAULT,
  873. 'period' => OtpTestData::PERIOD_DEFAULT,
  874. 'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
  875. 'icon' => '',
  876. ]);
  877. $response = $this->actingAs($this->user, 'api-guard')
  878. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '/otp')
  879. ->assertOk()
  880. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
  881. ->assertJsonFragment([
  882. 'otp_type' => 'totp',
  883. 'period' => OtpTestData::PERIOD_DEFAULT,
  884. ]);
  885. }
  886. #[Test]
  887. public function test_get_otp_by_posting_totp_uri_returns_consistent_resource()
  888. {
  889. $response = $this->actingAs($this->user, 'api-guard')
  890. ->json('POST', '/api/v1/twofaccounts/otp', [
  891. 'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
  892. ])
  893. ->assertOk()
  894. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
  895. ->assertJsonFragment([
  896. 'otp_type' => 'totp',
  897. 'period' => OtpTestData::PERIOD_CUSTOM,
  898. ]);
  899. }
  900. #[Test]
  901. public function test_get_otp_by_posting_totp_parameters_returns_consistent_resource()
  902. {
  903. $response = $this->actingAs($this->user, 'api-guard')
  904. ->json('POST', '/api/v1/twofaccounts/otp', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  905. ->assertOk()
  906. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
  907. ->assertJsonFragment([
  908. 'otp_type' => 'totp',
  909. 'period' => OtpTestData::PERIOD_CUSTOM,
  910. ]);
  911. }
  912. #[Test]
  913. public function test_get_otp_using_hotp_twofaccount_id_returns_consistent_resource()
  914. {
  915. $twofaccount = TwoFAccount::factory()->for($this->user)->create([
  916. 'otp_type' => 'hotp',
  917. 'account' => OtpTestData::ACCOUNT,
  918. 'service' => OtpTestData::SERVICE,
  919. 'secret' => OtpTestData::SECRET,
  920. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  921. 'digits' => OtpTestData::DIGITS_DEFAULT,
  922. 'period' => null,
  923. 'legacy_uri' => OtpTestData::HOTP_SHORT_URI,
  924. 'icon' => '',
  925. ]);
  926. $response = $this->actingAs($this->user, 'api-guard')
  927. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '/otp')
  928. ->assertOk()
  929. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
  930. ->assertJsonFragment([
  931. 'otp_type' => 'hotp',
  932. 'counter' => OtpTestData::COUNTER_DEFAULT + 1,
  933. ]);
  934. }
  935. #[Test]
  936. public function test_get_otp_by_posting_hotp_uri_returns_consistent_resource()
  937. {
  938. $response = $this->actingAs($this->user, 'api-guard')
  939. ->json('POST', '/api/v1/twofaccounts/otp', [
  940. 'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
  941. ])
  942. ->assertOk()
  943. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
  944. ->assertJsonFragment([
  945. 'otp_type' => 'hotp',
  946. 'counter' => OtpTestData::COUNTER_CUSTOM + 1,
  947. ]);
  948. }
  949. #[Test]
  950. public function test_get_otp_by_posting_hotp_parameters_returns_consistent_resource()
  951. {
  952. $response = $this->actingAs($this->user, 'api-guard')
  953. ->json('POST', '/api/v1/twofaccounts/otp', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
  954. ->assertOk()
  955. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
  956. ->assertJsonFragment([
  957. 'otp_type' => 'hotp',
  958. 'counter' => OtpTestData::COUNTER_CUSTOM + 1,
  959. ]);
  960. }
  961. #[Test]
  962. public function test_get_otp_by_posting_multiple_inputs_returns_bad_request()
  963. {
  964. $response = $this->actingAs($this->user, 'api-guard')
  965. ->json('POST', '/api/v1/twofaccounts/otp', [
  966. 'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
  967. 'key' => 'value',
  968. ])
  969. ->assertStatus(400)
  970. ->assertJsonStructure([
  971. 'message',
  972. 'reason',
  973. ]);
  974. }
  975. #[Test]
  976. public function test_get_otp_using_indecipherable_twofaccount_id_returns_bad_request()
  977. {
  978. Settings::set('useEncryption', true);
  979. $twofaccount = TwoFAccount::factory()->for($this->user)->create();
  980. DB::table('twofaccounts')
  981. ->where('id', $twofaccount->id)
  982. ->update([
  983. 'secret' => '**encrypted**',
  984. ]);
  985. $response = $this->actingAs($this->user, 'api-guard')
  986. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '/otp')
  987. ->assertStatus(400)
  988. ->assertJsonStructure([
  989. 'message',
  990. ]);
  991. }
  992. #[Test]
  993. public function test_get_otp_using_missing_twofaccount_id_returns_not_found()
  994. {
  995. $response = $this->actingAs($this->user, 'api-guard')
  996. ->json('GET', '/api/v1/twofaccounts/1000/otp')
  997. ->assertNotFound();
  998. }
  999. #[Test]
  1000. public function test_get_otp_by_posting_invalid_uri_returns_validation_error()
  1001. {
  1002. $response = $this->actingAs($this->user, 'api-guard')
  1003. ->json('POST', '/api/v1/twofaccounts/otp', [
  1004. 'uri' => OtpTestData::INVALID_OTPAUTH_URI,
  1005. ])
  1006. ->assertStatus(422);
  1007. }
  1008. #[Test]
  1009. public function test_get_otp_by_posting_invalid_parameters_returns_validation_error()
  1010. {
  1011. $response = $this->actingAs($this->user, 'api-guard')
  1012. ->json('POST', '/api/v1/twofaccounts/otp', self::ARRAY_OF_INVALID_PARAMETERS)
  1013. ->assertStatus(422);
  1014. }
  1015. #[Test]
  1016. public function test_get_otp_of_another_user_twofaccount_is_forbidden()
  1017. {
  1018. $response = $this->actingAs($this->user, 'api-guard')
  1019. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountC->id . '/otp')
  1020. ->assertForbidden()
  1021. ->assertJsonStructure([
  1022. 'message',
  1023. ]);
  1024. }
  1025. #[Test]
  1026. public function test_count_returns_right_number_of_twofaccounts()
  1027. {
  1028. $response = $this->actingAs($this->user, 'api-guard')
  1029. ->json('GET', '/api/v1/twofaccounts/count')
  1030. ->assertStatus(200)
  1031. ->assertExactJson([
  1032. 'count' => 2,
  1033. ]);
  1034. }
  1035. #[Test]
  1036. public function test_withdraw_returns_success()
  1037. {
  1038. $response = $this->actingAs($this->user, 'api-guard')
  1039. ->json('PATCH', '/api/v1/twofaccounts/withdraw?ids=1,2')
  1040. ->assertOk()
  1041. ->assertJsonStructure([
  1042. 'message',
  1043. ]);
  1044. }
  1045. #[Test]
  1046. public function test_withdraw_too_many_ids_returns_bad_request()
  1047. {
  1048. TwoFAccount::factory()->count(102)->for($this->user)->create();
  1049. $ids = DB::table('twofaccounts')->where('user_id', $this->user->id)->pluck('id')->implode(',');
  1050. $response = $this->actingAs($this->user, 'api-guard')
  1051. ->json('PATCH', '/api/v1/twofaccounts/withdraw?ids=' . $ids)
  1052. ->assertStatus(400)
  1053. ->assertJsonStructure([
  1054. 'message',
  1055. 'reason',
  1056. ]);
  1057. }
  1058. #[Test]
  1059. public function test_destroy_twofaccount_returns_success()
  1060. {
  1061. $response = $this->actingAs($this->user, 'api-guard')
  1062. ->json('DELETE', '/api/v1/twofaccounts/' . $this->twofaccountA->id)
  1063. ->assertNoContent();
  1064. }
  1065. #[Test]
  1066. public function test_destroy_missing_twofaccount_returns_not_found()
  1067. {
  1068. $response = $this->actingAs($this->user, 'api-guard')
  1069. ->json('DELETE', '/api/v1/twofaccounts/1000')
  1070. ->assertNotFound();
  1071. }
  1072. #[Test]
  1073. public function test_destroy_twofaccount_of_another_user_is_forbidden()
  1074. {
  1075. $response = $this->actingAs($this->user, 'api-guard')
  1076. ->json('DELETE', '/api/v1/twofaccounts/' . $this->twofaccountC->id)
  1077. ->assertForbidden()
  1078. ->assertJsonStructure([
  1079. 'message',
  1080. ]);
  1081. }
  1082. #[Test]
  1083. public function test_batch_destroy_twofaccount_returns_success()
  1084. {
  1085. TwoFAccount::factory()->count(3)->for($this->user)->create();
  1086. $response = $this->actingAs($this->user, 'api-guard')
  1087. ->json('DELETE', '/api/v1/twofaccounts?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountB->id)
  1088. ->assertNoContent();
  1089. }
  1090. #[Test]
  1091. public function test_batch_destroy_too_many_twofaccounts_returns_bad_request()
  1092. {
  1093. TwoFAccount::factory()->count(102)->for($this->user)->create();
  1094. $ids = DB::table('twofaccounts')->where('user_id', $this->user->id)->pluck('id')->implode(',');
  1095. $response = $this->actingAs($this->user, 'api-guard')
  1096. ->json('DELETE', '/api/v1/twofaccounts?ids=' . $ids)
  1097. ->assertStatus(400)
  1098. ->assertJsonStructure([
  1099. 'message',
  1100. 'reason',
  1101. ]);
  1102. }
  1103. #[Test]
  1104. public function test_batch_destroy_twofaccount_of_another_user_is_forbidden()
  1105. {
  1106. TwoFAccount::factory()->count(2)->for($this->anotherUser)->create();
  1107. $ids = DB::table('twofaccounts')
  1108. ->where('user_id', $this->anotherUser->id)
  1109. ->pluck('id')
  1110. ->implode(',');
  1111. $response = $this->actingAs($this->user, 'api-guard')
  1112. ->json('DELETE', '/api/v1/twofaccounts?ids=' . $ids)
  1113. ->assertForbidden()
  1114. ->assertJsonStructure([
  1115. 'message',
  1116. ]);
  1117. }
  1118. }