TwoFAccountControllerTest.php 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648
  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\IconStore;
  10. use App\Facades\Settings;
  11. use App\Models\Group;
  12. use App\Models\TwoFAccount;
  13. use App\Models\User;
  14. use App\Policies\TwoFAccountPolicy;
  15. use App\Providers\MigrationServiceProvider;
  16. use App\Providers\TwoFAuthServiceProvider;
  17. use App\Services\LogoLib\TfaLogoLib;
  18. use Illuminate\Http\Testing\FileFactory;
  19. use Illuminate\Support\Facades\DB;
  20. use Illuminate\Support\Facades\Http;
  21. use Illuminate\Support\Facades\Storage;
  22. use PHPUnit\Framework\Attributes\CoversClass;
  23. use PHPUnit\Framework\Attributes\DataProvider;
  24. use PHPUnit\Framework\Attributes\Test;
  25. use Tests\Classes\LocalFile;
  26. use Tests\Data\CommonDataProvider;
  27. use Tests\Data\HttpRequestTestData;
  28. use Tests\Data\MigrationTestData;
  29. use Tests\Data\OtpTestData;
  30. use Tests\FeatureTestCase;
  31. /**
  32. * TwoFAccountControllerTest test class
  33. */
  34. #[CoversClass(TwoFAccountController::class)]
  35. #[CoversClass(TwoFAccountCollection::class)]
  36. #[CoversClass(TwoFAccountReadResource::class)]
  37. #[CoversClass(TwoFAccountStoreResource::class)]
  38. #[CoversClass(TwoFAccountExportResource::class)]
  39. #[CoversClass(TwoFAccountExportCollection::class)]
  40. #[CoversClass(MigrationServiceProvider::class)]
  41. #[CoversClass(TwoFAuthServiceProvider::class)]
  42. #[CoversClass(TwoFAccountPolicy::class)]
  43. class TwoFAccountControllerTest extends FeatureTestCase
  44. {
  45. /**
  46. * @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
  47. */
  48. protected $user;
  49. protected $anotherUser;
  50. /**
  51. * @var App\Models\Group
  52. */
  53. protected $userGroupA;
  54. protected $userGroupB;
  55. protected $anotherUserGroupA;
  56. protected $anotherUserGroupB;
  57. /**
  58. * @var App\Models\TwoFAccount
  59. */
  60. protected $twofaccountA;
  61. protected $twofaccountB;
  62. protected $twofaccountC;
  63. protected $twofaccountD;
  64. protected $twofaccountE;
  65. private const VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET = [
  66. 'id',
  67. 'group_id',
  68. 'service',
  69. 'account',
  70. 'icon',
  71. 'otp_type',
  72. 'digits',
  73. 'algorithm',
  74. 'period',
  75. 'counter',
  76. ];
  77. private const VALID_RESOURCE_STRUCTURE_WITH_SECRET = [
  78. 'id',
  79. 'group_id',
  80. 'service',
  81. 'account',
  82. 'icon',
  83. 'otp_type',
  84. 'secret',
  85. 'digits',
  86. 'algorithm',
  87. 'period',
  88. 'counter',
  89. ];
  90. private const VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP = [
  91. 'generated_at',
  92. 'otp_type',
  93. 'password',
  94. 'period',
  95. ];
  96. private const VALID_EMBEDDED_OTP_RESOURCE_STRUCTURE_FOR_TOTP = [
  97. 'generated_at',
  98. 'password',
  99. 'next_password',
  100. ];
  101. private const VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP = [
  102. 'otp_type',
  103. 'password',
  104. 'counter',
  105. ];
  106. private const VALID_RESOURCE_STRUCTURE_WITH_OTP = [
  107. 'id',
  108. 'group_id',
  109. 'service',
  110. 'account',
  111. 'icon',
  112. 'otp_type',
  113. 'secret',
  114. 'digits',
  115. 'algorithm',
  116. 'period',
  117. 'counter',
  118. 'otp' => self::VALID_EMBEDDED_OTP_RESOURCE_STRUCTURE_FOR_TOTP,
  119. ];
  120. private const VALID_COLLECTION_RESOURCE_STRUCTURE_WITH_OTP = [
  121. 'id',
  122. 'group_id',
  123. 'service',
  124. 'account',
  125. 'icon',
  126. 'otp_type',
  127. 'digits',
  128. 'algorithm',
  129. 'period',
  130. 'counter',
  131. 'otp' => self::VALID_EMBEDDED_OTP_RESOURCE_STRUCTURE_FOR_TOTP,
  132. ];
  133. private const VALID_EXPORT_STRUTURE = [
  134. 'app',
  135. 'schema',
  136. 'datetime',
  137. 'data' => [
  138. '*' => [
  139. 'otp_type',
  140. 'account',
  141. 'service',
  142. 'icon',
  143. 'icon_mime',
  144. 'icon_file',
  145. 'secret',
  146. 'digits',
  147. 'algorithm',
  148. 'period',
  149. 'counter',
  150. 'legacy_uri',
  151. ],
  152. ],
  153. ];
  154. private const VALID_EXPORT_AS_URIS_STRUTURE = [
  155. 'app',
  156. 'datetime',
  157. 'data' => [
  158. '*' => [
  159. 'uri',
  160. ],
  161. ],
  162. ];
  163. private const JSON_FRAGMENTS_FOR_CUSTOM_TOTP = [
  164. 'service' => OtpTestData::SERVICE,
  165. 'account' => OtpTestData::ACCOUNT,
  166. 'otp_type' => 'totp',
  167. 'secret' => OtpTestData::SECRET,
  168. 'digits' => OtpTestData::DIGITS_CUSTOM,
  169. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  170. 'period' => OtpTestData::PERIOD_CUSTOM,
  171. 'counter' => null,
  172. ];
  173. private const JSON_FRAGMENTS_FOR_DEFAULT_TOTP = [
  174. 'service' => null,
  175. 'account' => OtpTestData::ACCOUNT,
  176. 'otp_type' => 'totp',
  177. 'secret' => OtpTestData::SECRET,
  178. 'digits' => OtpTestData::DIGITS_DEFAULT,
  179. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  180. 'period' => OtpTestData::PERIOD_DEFAULT,
  181. 'counter' => null,
  182. ];
  183. private const JSON_FRAGMENTS_FOR_CUSTOM_HOTP = [
  184. 'service' => OtpTestData::SERVICE,
  185. 'account' => OtpTestData::ACCOUNT,
  186. 'otp_type' => 'hotp',
  187. 'secret' => OtpTestData::SECRET,
  188. 'digits' => OtpTestData::DIGITS_CUSTOM,
  189. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  190. 'period' => null,
  191. 'counter' => OtpTestData::COUNTER_CUSTOM,
  192. ];
  193. private const JSON_FRAGMENTS_FOR_DEFAULT_HOTP = [
  194. 'service' => null,
  195. 'account' => OtpTestData::ACCOUNT,
  196. 'otp_type' => 'hotp',
  197. 'secret' => OtpTestData::SECRET,
  198. 'digits' => OtpTestData::DIGITS_DEFAULT,
  199. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  200. 'period' => null,
  201. 'counter' => OtpTestData::COUNTER_DEFAULT,
  202. ];
  203. private const ARRAY_OF_INVALID_PARAMETERS = [
  204. 'account' => null,
  205. 'otp_type' => 'totp',
  206. 'secret' => OtpTestData::SECRET,
  207. ];
  208. public function setUp() : void
  209. {
  210. parent::setUp();
  211. Storage::fake('icons');
  212. Storage::fake('logos');
  213. Storage::fake('imagesLink');
  214. Http::preventStrayRequests();
  215. Http::fake([
  216. CommonDataProvider::TFA_URL => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
  217. CommonDataProvider::SELFH_URL => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
  218. CommonDataProvider::DASHBOARDICONS_URL => Http::response(HttpRequestTestData::SVG_LOGO_BODY, 200),
  219. TfaLogoLib::TFA_JSON_URL => Http::response(HttpRequestTestData::TFA_JSON_BODY, 200),
  220. OtpTestData::EXTERNAL_IMAGE_URL_DECODED => Http::response((new FileFactory)->image('file.png', 10, 10)->tempFile, 200),
  221. OtpTestData::EXTERNAL_INFECTED_IMAGE_URL_DECODED => Http::response((new FileFactory)->createWithContent('infected.svg', OtpTestData::ICON_SVG_DATA_INFECTED)->tempFile, 200),
  222. 'example.com/*' => Http::response(null, 400),
  223. ]);
  224. $this->user = User::factory()->create();
  225. $this->userGroupA = Group::factory()->for($this->user)->create();
  226. $this->userGroupB = Group::factory()->for($this->user)->create();
  227. $this->twofaccountA = TwoFAccount::factory()->for($this->user)->create([
  228. 'group_id' => $this->userGroupA->id,
  229. ]);
  230. $this->twofaccountB = TwoFAccount::factory()->for($this->user)->create([
  231. 'group_id' => $this->userGroupA->id,
  232. ]);
  233. $this->anotherUser = User::factory()->create();
  234. $this->anotherUserGroupA = Group::factory()->for($this->anotherUser)->create();
  235. $this->anotherUserGroupB = Group::factory()->for($this->anotherUser)->create();
  236. $this->twofaccountC = TwoFAccount::factory()->for($this->anotherUser)->create([
  237. 'group_id' => $this->anotherUserGroupA->id,
  238. ]);
  239. $this->twofaccountD = TwoFAccount::factory()->for($this->anotherUser)->create([
  240. 'group_id' => $this->anotherUserGroupB->id,
  241. ]);
  242. $this->twofaccountE = TwoFAccount::factory()->for($this->anotherUser)->create([
  243. 'group_id' => $this->anotherUserGroupB->id,
  244. ]);
  245. }
  246. #[Test]
  247. #[DataProvider('validResourceStructureProvider')]
  248. public function test_index_returns_user_twofaccounts_only($urlParameter, $expected)
  249. {
  250. $response = $this->actingAs($this->user, 'api-guard')
  251. ->json('GET', '/api/v1/twofaccounts' . $urlParameter)
  252. ->assertOk()
  253. ->assertJsonCount(2, $key = null)
  254. ->assertJsonStructure([
  255. '*' => $expected,
  256. ])
  257. ->assertJsonFragment([
  258. 'id' => $this->twofaccountA->id,
  259. ])
  260. ->assertJsonFragment([
  261. 'id' => $this->twofaccountB->id,
  262. ])
  263. ->assertJsonMissing([
  264. 'id' => $this->twofaccountC->id,
  265. ])
  266. ->assertJsonMissing([
  267. 'id' => $this->twofaccountD->id,
  268. ]);
  269. }
  270. /**
  271. * Provide data for index tests
  272. */
  273. public static function validResourceStructureProvider()
  274. {
  275. return [
  276. 'VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET' => [
  277. '',
  278. self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET,
  279. ],
  280. 'VALID_RESOURCE_STRUCTURE_WITH_SECRET' => [
  281. '?withSecret=1',
  282. self::VALID_RESOURCE_STRUCTURE_WITH_SECRET,
  283. ],
  284. 'VALID_COLLECTION_RESOURCE_STRUCTURE_WITH_OTP' => [
  285. '?withOtp=1',
  286. self::VALID_COLLECTION_RESOURCE_STRUCTURE_WITH_OTP,
  287. ],
  288. ];
  289. }
  290. #[Test]
  291. public function test_index_returns_user_accounts_with_given_ids()
  292. {
  293. $response = $this->actingAs($this->anotherUser, 'api-guard')
  294. ->json('GET', '/api/v1/twofaccounts?ids=' . $this->twofaccountC->id . ',' . $this->twofaccountE->id)
  295. ->assertOk()
  296. ->assertJsonCount(2, $key = null)
  297. ->assertJsonStructure([
  298. '*' => self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET,
  299. ])
  300. ->assertJsonFragment([
  301. 'id' => $this->twofaccountC->id,
  302. ])
  303. ->assertJsonFragment([
  304. 'id' => $this->twofaccountE->id,
  305. ]);
  306. }
  307. #[Test]
  308. public function test_index_returns_only_user_accounts_in_given_ids()
  309. {
  310. $response = $this->actingAs($this->anotherUser, 'api-guard')
  311. ->json('GET', '/api/v1/twofaccounts?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountE->id)
  312. ->assertOk()
  313. ->assertJsonCount(1, $key = null)
  314. ->assertJsonStructure([
  315. '*' => self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET,
  316. ])
  317. ->assertJsonMissing([
  318. 'id' => $this->twofaccountA->id,
  319. ])
  320. ->assertJsonFragment([
  321. 'id' => $this->twofaccountE->id,
  322. ]);
  323. }
  324. #[Test]
  325. public function test_orphan_accounts_are_reassign_to_the_only_user()
  326. {
  327. config(['auth.defaults.guard' => 'reverse-proxy-guard']);
  328. $this->anotherUser->delete();
  329. $this->twofaccountA->user_id = null;
  330. $this->twofaccountA->save();
  331. $this->assertCount(1, User::all());
  332. $this->assertNull($this->twofaccountA->user_id);
  333. $this->assertCount(1, TwoFAccount::orphans()->get());
  334. $this->actingAs($this->user, 'reverse-proxy-guard')
  335. ->json('GET', '/api/v1/twofaccounts')
  336. ->assertOk();
  337. $this->twofaccountA->refresh();
  338. $this->assertNotNull($this->twofaccountA->user_id);
  339. }
  340. #[Test]
  341. public function test_show_returns_twofaccount_resource_with_secret()
  342. {
  343. $response = $this->actingAs($this->user, 'api-guard')
  344. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountA->id)
  345. ->assertOk()
  346. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_SECRET);
  347. }
  348. #[Test]
  349. public function test_show_returns_twofaccount_resource_without_secret()
  350. {
  351. $response = $this->actingAs($this->user, 'api-guard')
  352. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountA->id . '?withSecret=0')
  353. ->assertOk()
  354. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET);
  355. }
  356. //#[Test]
  357. // public function test_show_twofaccount_with_indeciphered_data_returns_replaced_data()
  358. // {
  359. // $dbEncryptionService = resolve('App\Services\DbEncryptionService');
  360. // $dbEncryptionService->setTo(true);
  361. // $twofaccount = TwoFAccount::factory()->create();
  362. // DB::table('twofaccounts')
  363. // ->where('id', $twofaccount->id)
  364. // ->update([
  365. // 'secret' => '**encrypted**',
  366. // 'account' => '**encrypted**',
  367. // ]);
  368. // $response = $this->actingAs($this->user, 'api-guard')
  369. // ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id)
  370. // ->assertJsonFragment([
  371. // 'secret' => '*indecipherable*',
  372. // 'account' => '*indecipherable*',
  373. // ]);
  374. // }
  375. #[Test]
  376. public function test_show_returns_twofaccount_resource_with_otp()
  377. {
  378. $response = $this->actingAs($this->user, 'api-guard')
  379. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountA->id . '?withOtp=1')
  380. ->assertOk()
  381. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_OTP);
  382. }
  383. #[Test]
  384. public function test_show_returns_twofaccount_resource_without_otp()
  385. {
  386. $response = $this->actingAs($this->user, 'api-guard')
  387. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountA->id . '?withOtp=0')
  388. ->assertOk()
  389. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET);
  390. }
  391. #[Test]
  392. public function test_show_missing_twofaccount_returns_not_found()
  393. {
  394. $response = $this->actingAs($this->user, 'api-guard')
  395. ->json('GET', '/api/v1/twofaccounts/1000')
  396. ->assertNotFound()
  397. ->assertJsonStructure([
  398. 'message',
  399. ]);
  400. }
  401. #[Test]
  402. public function test_show_twofaccount_of_another_user_is_forbidden()
  403. {
  404. $response = $this->actingAs($this->user, 'api-guard')
  405. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountC->id)
  406. ->assertForbidden()
  407. ->assertJsonStructure([
  408. 'message',
  409. ]);
  410. }
  411. #[Test]
  412. #[DataProvider('accountCreationProvider')]
  413. public function test_store_without_encryption_returns_success_with_consistent_resource_structure($payload, $expected)
  414. {
  415. Settings::set('useEncryption', false);
  416. $response = $this->actingAs($this->user, 'api-guard')
  417. ->json('POST', '/api/v1/twofaccounts', $payload)
  418. ->assertCreated()
  419. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_SECRET)
  420. ->assertJsonFragment($expected);
  421. }
  422. #[Test]
  423. #[DataProvider('accountCreationProvider')]
  424. public function test_store_with_encryption_returns_success_with_consistent_resource_structure($payload, $expected)
  425. {
  426. Settings::set('useEncryption', true);
  427. $response = $this->actingAs($this->user, 'api-guard')
  428. ->json('POST', '/api/v1/twofaccounts', $payload)
  429. ->assertCreated()
  430. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_SECRET)
  431. ->assertJsonFragment($expected);
  432. }
  433. /**
  434. * Provide data for TwoFAccount store tests
  435. */
  436. public static function accountCreationProvider()
  437. {
  438. return [
  439. 'TOTP_FULL_CUSTOM_URI' => [
  440. [
  441. 'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
  442. ],
  443. self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP,
  444. ],
  445. 'TOTP_SHORT_URI' => [
  446. [
  447. 'uri' => OtpTestData::TOTP_SHORT_URI,
  448. ],
  449. self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP,
  450. ],
  451. 'ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP' => [
  452. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  453. self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP,
  454. ],
  455. 'ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP' => [
  456. OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP,
  457. self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP,
  458. ],
  459. 'HOTP_FULL_CUSTOM_URI' => [
  460. [
  461. 'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
  462. ],
  463. self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP,
  464. ],
  465. 'HOTP_SHORT_URI' => [
  466. [
  467. 'uri' => OtpTestData::HOTP_SHORT_URI,
  468. ],
  469. self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP,
  470. ],
  471. 'ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP' => [
  472. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP,
  473. self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP,
  474. ],
  475. 'ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP' => [
  476. OtpTestData::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP,
  477. self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP,
  478. ],
  479. ];
  480. }
  481. #[Test]
  482. public function test_store_with_invalid_uri_returns_validation_error()
  483. {
  484. $response = $this->actingAs($this->user, 'api-guard')
  485. ->json('POST', '/api/v1/twofaccounts', [
  486. 'uri' => OtpTestData::INVALID_OTPAUTH_URI,
  487. ])
  488. ->assertStatus(422);
  489. }
  490. #[Test]
  491. public function test_store_assigns_created_account_to_provided_groupid()
  492. {
  493. // Set the default group to No group
  494. $this->user['preferences->defaultGroup'] = 0;
  495. $this->user->save();
  496. $response = $this->actingAs($this->user, 'api-guard')
  497. ->json('POST', '/api/v1/twofaccounts', array_merge(
  498. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  499. ['group_id' => $this->userGroupA->id]
  500. ))
  501. ->assertJsonFragment([
  502. 'group_id' => $this->userGroupA->id,
  503. ]);
  504. }
  505. #[Test]
  506. public function test_store_with_assignement_to_missing_groupid_returns_validation_error()
  507. {
  508. $response = $this->actingAs($this->user, 'api-guard')
  509. ->json('POST', '/api/v1/twofaccounts', array_merge(
  510. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  511. ['group_id' => 9999999]
  512. ))
  513. ->assertJsonValidationErrorFor('group_id');
  514. }
  515. #[Test]
  516. public function test_store_with_assignement_to_null_groupid_does_not_assign_account_to_group()
  517. {
  518. // Set the default group to No group
  519. $this->user['preferences->defaultGroup'] = 0;
  520. $this->user->save();
  521. $response = $this->actingAs($this->user, 'api-guard')
  522. ->json('POST', '/api/v1/twofaccounts', array_merge(
  523. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  524. ['group_id' => null]
  525. ))
  526. ->assertJsonFragment([
  527. 'group_id' => null,
  528. ]);
  529. }
  530. #[Test]
  531. public function test_store_with_assignement_to_null_groupid_is_overriden_by_specific_default_group()
  532. {
  533. // Set the default group to a specific group
  534. $this->user['preferences->defaultGroup'] = $this->userGroupA->id;
  535. $this->user->save();
  536. $response = $this->actingAs($this->user, 'api-guard')
  537. ->json('POST', '/api/v1/twofaccounts', array_merge(
  538. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  539. ['group_id' => null]
  540. ))
  541. ->assertJsonFragment([
  542. 'group_id' => $this->user->preferences['defaultGroup'],
  543. ]);
  544. }
  545. #[Test]
  546. public function test_store_with_assignement_to_zero_groupid_overrides_specific_default_group()
  547. {
  548. // Set the default group to a specific group
  549. $this->user['preferences->defaultGroup'] = $this->userGroupA->id;
  550. $this->user->save();
  551. $response = $this->actingAs($this->user, 'api-guard')
  552. ->json('POST', '/api/v1/twofaccounts', array_merge(
  553. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  554. ['group_id' => 0]
  555. ))
  556. ->assertJsonFragment([
  557. 'group_id' => null,
  558. ]);
  559. }
  560. #[Test]
  561. public function test_store_with_assignement_to_provided_groupid_overrides_specific_default_group()
  562. {
  563. // Set the default group to a specific group
  564. $this->user['preferences->defaultGroup'] = $this->userGroupA->id;
  565. $this->user->save();
  566. $response = $this->actingAs($this->user, 'api-guard')
  567. ->json('POST', '/api/v1/twofaccounts', array_merge(
  568. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  569. ['group_id' => $this->userGroupB->id]
  570. ))
  571. ->assertJsonFragment([
  572. 'group_id' => $this->userGroupB->id,
  573. ]);
  574. }
  575. #[Test]
  576. public function test_store_assigns_created_account_when_default_group_is_a_specific_one()
  577. {
  578. // Set the default group to a specific one
  579. $this->user['preferences->defaultGroup'] = $this->userGroupA->id;
  580. $this->user->save();
  581. $response = $this->actingAs($this->user, 'api-guard')
  582. ->json('POST', '/api/v1/twofaccounts', [
  583. 'uri' => OtpTestData::TOTP_SHORT_URI,
  584. ])
  585. ->assertJsonFragment([
  586. 'group_id' => $this->user->preferences['defaultGroup'],
  587. ]);
  588. }
  589. #[Test]
  590. public function test_store_assigns_created_account_when_default_group_is_the_active_one()
  591. {
  592. // Set the default group to be the active one
  593. $this->user['preferences->defaultGroup'] = -1;
  594. // Set the active group
  595. $this->user['preferences->activeGroup'] = $this->userGroupA->id;
  596. $this->user->save();
  597. $response = $this->actingAs($this->user, 'api-guard')
  598. ->json('POST', '/api/v1/twofaccounts', [
  599. 'uri' => OtpTestData::TOTP_SHORT_URI,
  600. ])
  601. ->assertJsonFragment([
  602. 'group_id' => $this->user->preferences['activeGroup'],
  603. ]);
  604. }
  605. #[Test]
  606. public function test_store_assigns_created_account_when_default_group_is_no_group()
  607. {
  608. // Set the default group to No group
  609. $this->user['preferences->defaultGroup'] = 0;
  610. $this->user->save();
  611. $response = $this->actingAs($this->user, 'api-guard')
  612. ->json('POST', '/api/v1/twofaccounts', [
  613. 'uri' => OtpTestData::TOTP_SHORT_URI,
  614. ])
  615. ->assertJsonFragment([
  616. 'group_id' => null,
  617. ]);
  618. }
  619. #[Test]
  620. public function test_store_assigns_created_account_when_default_group_does_not_exist()
  621. {
  622. // Set the default group to a non-existing one
  623. $this->user['preferences->defaultGroup'] = 1000;
  624. $this->user->save();
  625. $response = $this->actingAs($this->user, 'api-guard')
  626. ->json('POST', '/api/v1/twofaccounts', [
  627. 'uri' => OtpTestData::TOTP_SHORT_URI,
  628. ])
  629. ->assertJsonFragment([
  630. 'group_id' => null,
  631. ]);
  632. }
  633. #[Test]
  634. public function test_update_totp_returns_success_with_updated_resource()
  635. {
  636. $response = $this->actingAs($this->user, 'api-guard')
  637. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  638. ->assertOk()
  639. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  640. }
  641. #[Test]
  642. public function test_update_hotp_returns_success_with_updated_resource()
  643. {
  644. $response = $this->actingAs($this->user, 'api-guard')
  645. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
  646. ->assertOk()
  647. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP);
  648. }
  649. #[Test]
  650. public function test_update_missing_twofaccount_returns_not_found()
  651. {
  652. $response = $this->actingAs($this->user, 'api-guard')
  653. ->json('PUT', '/api/v1/twofaccounts/1000', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  654. ->assertNotFound();
  655. }
  656. #[Test]
  657. public function test_update_with_assignement_to_null_group_returns_success_with_updated_resource()
  658. {
  659. $this->assertNotEquals(null, $this->twofaccountA->group_id);
  660. $response = $this->actingAs($this->user, 'api-guard')
  661. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, array_merge(
  662. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  663. ['group_id' => null]
  664. ))
  665. ->assertOk()
  666. ->assertJsonFragment([
  667. 'group_id' => null,
  668. ])
  669. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  670. }
  671. #[Test]
  672. public function test_update_with_assignement_to_zero_group_returns_success_with_updated_resource()
  673. {
  674. $this->assertNotEquals(null, $this->twofaccountA->group_id);
  675. $response = $this->actingAs($this->user, 'api-guard')
  676. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, array_merge(
  677. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  678. ['group_id' => 0]
  679. ))
  680. ->assertOk()
  681. ->assertJsonFragment([
  682. 'group_id' => null,
  683. ])
  684. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  685. }
  686. #[Test]
  687. public function test_update_with_assignement_to_new_groupid_returns_success_with_updated_resource()
  688. {
  689. $this->assertEquals($this->userGroupA->id, $this->twofaccountA->group_id);
  690. $response = $this->actingAs($this->user, 'api-guard')
  691. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, array_merge(
  692. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  693. ['group_id' => $this->userGroupB->id]
  694. ))
  695. ->assertOk()
  696. ->assertJsonFragment([
  697. 'group_id' => $this->userGroupB->id,
  698. ])
  699. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  700. }
  701. #[Test]
  702. public function test_update_with_assignement_to_missing_groupid_returns_validation_error()
  703. {
  704. $response = $this->actingAs($this->user, 'api-guard')
  705. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, array_merge(
  706. OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP,
  707. ['group_id' => 9999999]
  708. ))
  709. ->assertJsonValidationErrorFor('group_id');
  710. }
  711. #[Test]
  712. public function test_update_twofaccount_with_invalid_data_returns_validation_error()
  713. {
  714. $twofaccount = TwoFAccount::factory()->create();
  715. $response = $this->actingAs($this->user, 'api-guard')
  716. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountA->id, self::ARRAY_OF_INVALID_PARAMETERS)
  717. ->assertStatus(422);
  718. }
  719. #[Test]
  720. public function test_update_twofaccount_of_another_user_is_forbidden()
  721. {
  722. $response = $this->actingAs($this->user, 'api-guard')
  723. ->json('PUT', '/api/v1/twofaccounts/' . $this->twofaccountC->id, OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
  724. ->assertForbidden()
  725. ->assertJsonStructure([
  726. 'message',
  727. ]);
  728. }
  729. #[Test]
  730. public function test_update_with_removed_icon_prevents_official_logo_fetching()
  731. {
  732. $attributes = ([
  733. 'otp_type' => 'totp',
  734. 'account' => OtpTestData::ACCOUNT,
  735. 'service' => OtpTestData::SERVICE,
  736. 'secret' => OtpTestData::SECRET,
  737. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  738. 'digits' => OtpTestData::DIGITS_DEFAULT,
  739. 'period' => OtpTestData::PERIOD_DEFAULT,
  740. 'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
  741. 'icon' => 'icon.png',
  742. ]);
  743. $twofaccount = TwoFAccount::factory()->for($this->user)->create($attributes);
  744. $attributes['icon'] = '';
  745. $response = $this->actingAs($this->user, 'api-guard')
  746. ->json('PUT', '/api/v1/twofaccounts/' . $twofaccount->id, $attributes);
  747. $this->assertNull($response->json('icon'));
  748. }
  749. #[Test]
  750. public function test_migrate_valid_gauth_payload_returns_success_with_consistent_resources()
  751. {
  752. $response = $this->actingAs($this->user, 'api-guard')
  753. ->json('POST', '/api/v1/twofaccounts/migration', [
  754. 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI,
  755. 'withSecret' => 1,
  756. ])
  757. ->assertOk()
  758. ->assertJsonCount(2, $key = null)
  759. ->assertJsonFragment([
  760. 'id' => 0,
  761. 'service' => OtpTestData::SERVICE,
  762. 'account' => OtpTestData::ACCOUNT,
  763. 'otp_type' => 'totp',
  764. 'secret' => OtpTestData::SECRET,
  765. 'digits' => OtpTestData::DIGITS_DEFAULT,
  766. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  767. 'period' => OtpTestData::PERIOD_DEFAULT,
  768. 'counter' => null,
  769. ])
  770. ->assertJsonFragment([
  771. 'id' => 0,
  772. 'service' => OtpTestData::SERVICE . '_bis',
  773. 'account' => OtpTestData::ACCOUNT . '_bis',
  774. 'otp_type' => 'totp',
  775. 'secret' => OtpTestData::SECRET,
  776. 'digits' => OtpTestData::DIGITS_DEFAULT,
  777. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  778. 'period' => OtpTestData::PERIOD_DEFAULT,
  779. 'counter' => null,
  780. ]);
  781. }
  782. #[Test]
  783. public function test_migrate_with_invalid_gauth_payload_returns_validation_error()
  784. {
  785. $response = $this->actingAs($this->user, 'api-guard')
  786. ->json('POST', '/api/v1/twofaccounts/migration', [
  787. 'uri' => MigrationTestData::INVALID_GOOGLE_AUTH_MIGRATION_URI,
  788. ])
  789. ->assertStatus(422);
  790. }
  791. #[Test]
  792. public function test_migrate_payload_with_duplicates_returns_negative_ids()
  793. {
  794. $twofaccount = TwoFAccount::factory()->for($this->user)->create([
  795. 'otp_type' => 'totp',
  796. 'account' => OtpTestData::ACCOUNT,
  797. 'service' => OtpTestData::SERVICE,
  798. 'secret' => OtpTestData::SECRET,
  799. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  800. 'digits' => OtpTestData::DIGITS_DEFAULT,
  801. 'period' => OtpTestData::PERIOD_DEFAULT,
  802. 'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
  803. 'icon' => '',
  804. ]);
  805. $response = $this->actingAs($this->user, 'api-guard')
  806. ->json('POST', '/api/v1/twofaccounts/migration?withSecret=1', [
  807. 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI,
  808. ])
  809. ->assertOk()
  810. ->assertJsonFragment([
  811. 'id' => -1,
  812. 'service' => OtpTestData::SERVICE,
  813. 'account' => OtpTestData::ACCOUNT,
  814. 'otp_type' => 'totp',
  815. 'secret' => OtpTestData::SECRET,
  816. 'digits' => OtpTestData::DIGITS_DEFAULT,
  817. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  818. 'period' => OtpTestData::PERIOD_DEFAULT,
  819. 'counter' => null,
  820. ])
  821. ->assertJsonFragment([
  822. 'id' => 0,
  823. 'service' => OtpTestData::SERVICE . '_bis',
  824. 'account' => OtpTestData::ACCOUNT . '_bis',
  825. 'otp_type' => 'totp',
  826. 'secret' => OtpTestData::SECRET,
  827. 'digits' => OtpTestData::DIGITS_DEFAULT,
  828. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  829. 'period' => OtpTestData::PERIOD_DEFAULT,
  830. 'counter' => null,
  831. ]);
  832. }
  833. #[Test]
  834. public function test_migrate_identifies_duplicates_in_authenticated_user_twofaccounts_only()
  835. {
  836. $this->user['preferences->getOfficialIcons'] = false;
  837. $twofaccount = TwoFAccount::factory()->for($this->anotherUser)->create([
  838. 'otp_type' => 'totp',
  839. 'account' => OtpTestData::ACCOUNT,
  840. 'service' => OtpTestData::SERVICE,
  841. 'secret' => OtpTestData::SECRET,
  842. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  843. 'digits' => OtpTestData::DIGITS_DEFAULT,
  844. 'period' => OtpTestData::PERIOD_DEFAULT,
  845. 'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
  846. 'icon' => '',
  847. ]);
  848. $response = $this->actingAs($this->user, 'api-guard')
  849. ->json('POST', '/api/v1/twofaccounts/migration?withSecret=1', [
  850. 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI,
  851. ])
  852. ->assertOk()
  853. ->assertJsonFragment([
  854. 'id' => 0,
  855. 'account' => OtpTestData::ACCOUNT,
  856. 'service' => OtpTestData::SERVICE,
  857. 'otp_type' => 'totp',
  858. 'secret' => OtpTestData::SECRET,
  859. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  860. 'digits' => OtpTestData::DIGITS_DEFAULT,
  861. 'period' => OtpTestData::PERIOD_DEFAULT,
  862. 'icon' => null,
  863. ])
  864. ->assertJsonFragment([
  865. 'id' => 0,
  866. 'service' => OtpTestData::SERVICE . '_bis',
  867. 'account' => OtpTestData::ACCOUNT . '_bis',
  868. 'otp_type' => 'totp',
  869. 'secret' => OtpTestData::SECRET,
  870. 'digits' => OtpTestData::DIGITS_DEFAULT,
  871. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  872. 'period' => OtpTestData::PERIOD_DEFAULT,
  873. 'counter' => null,
  874. ]);
  875. }
  876. #[Test]
  877. public function test_migrate_invalid_gauth_payload_returns_bad_request()
  878. {
  879. $response = $this->actingAs($this->user, 'api-guard')
  880. ->json('POST', '/api/v1/twofaccounts/migration', [
  881. 'payload' => MigrationTestData::GOOGLE_AUTH_MIGRATION_URI_WITH_INVALID_DATA,
  882. ])
  883. ->assertStatus(400)
  884. ->assertJsonStructure([
  885. 'message',
  886. ]);
  887. }
  888. #[Test]
  889. public function test_migrate_valid_aegis_json_file_returns_success()
  890. {
  891. $file = LocalFile::fake()->validAegisJsonFile();
  892. $response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
  893. ->actingAs($this->user, 'api-guard')
  894. ->json('POST', '/api/v1/twofaccounts/migration', [
  895. 'file' => $file,
  896. 'withSecret' => 1,
  897. ])
  898. ->assertOk()
  899. ->assertJsonCount(3, $key = null)
  900. ->assertJsonFragment([
  901. 'id' => 0,
  902. 'service' => OtpTestData::SERVICE,
  903. 'account' => OtpTestData::ACCOUNT,
  904. 'otp_type' => 'totp',
  905. 'secret' => OtpTestData::SECRET,
  906. 'digits' => OtpTestData::DIGITS_CUSTOM,
  907. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  908. 'period' => OtpTestData::PERIOD_CUSTOM,
  909. 'counter' => null,
  910. ])
  911. ->assertJsonFragment([
  912. 'id' => 0,
  913. 'service' => OtpTestData::SERVICE,
  914. 'account' => OtpTestData::ACCOUNT,
  915. 'otp_type' => 'hotp',
  916. 'secret' => OtpTestData::SECRET,
  917. 'digits' => OtpTestData::DIGITS_CUSTOM,
  918. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  919. 'period' => null,
  920. 'counter' => OtpTestData::COUNTER_CUSTOM,
  921. ])
  922. ->assertJsonFragment([
  923. 'id' => 0,
  924. 'service' => OtpTestData::STEAM,
  925. 'account' => OtpTestData::ACCOUNT,
  926. 'otp_type' => 'steamtotp',
  927. 'secret' => OtpTestData::STEAM_SECRET,
  928. 'digits' => OtpTestData::DIGITS_STEAM,
  929. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  930. 'period' => OtpTestData::PERIOD_DEFAULT,
  931. 'counter' => null,
  932. ]);
  933. }
  934. #[Test]
  935. #[DataProvider('invalidAegisJsonFileProvider')]
  936. public function test_migrate_invalid_aegis_json_file_returns_bad_request($file)
  937. {
  938. $response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
  939. ->actingAs($this->user, 'api-guard')
  940. ->json('POST', '/api/v1/twofaccounts/migration', [
  941. 'file' => $file,
  942. ])
  943. ->assertStatus(400);
  944. }
  945. /**
  946. * Provide invalid Aegis JSON files for import tests
  947. */
  948. public static function invalidAegisJsonFileProvider()
  949. {
  950. return [
  951. 'encryptedAegisJsonFile' => [
  952. LocalFile::fake()->encryptedAegisJsonFile(),
  953. ],
  954. 'invalidAegisJsonFile' => [
  955. LocalFile::fake()->invalidAegisJsonFile(),
  956. ],
  957. ];
  958. }
  959. #[Test]
  960. #[DataProvider('validPlainTextFileProvider')]
  961. public function test_migrate_valid_plain_text_file_returns_success($file)
  962. {
  963. $response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
  964. ->actingAs($this->user, 'api-guard')
  965. ->json('POST', '/api/v1/twofaccounts/migration', [
  966. 'file' => $file,
  967. 'withSecret' => 1,
  968. ])
  969. ->assertOk()
  970. ->assertJsonCount(3, $key = null)
  971. ->assertJsonFragment([
  972. 'id' => 0,
  973. 'service' => OtpTestData::SERVICE,
  974. 'account' => OtpTestData::ACCOUNT,
  975. 'otp_type' => 'totp',
  976. 'secret' => OtpTestData::SECRET,
  977. 'digits' => OtpTestData::DIGITS_CUSTOM,
  978. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  979. 'period' => OtpTestData::PERIOD_CUSTOM,
  980. 'counter' => null,
  981. ])
  982. ->assertJsonFragment([
  983. 'id' => 0,
  984. 'service' => OtpTestData::SERVICE,
  985. 'account' => OtpTestData::ACCOUNT,
  986. 'otp_type' => 'hotp',
  987. 'secret' => OtpTestData::SECRET,
  988. 'digits' => OtpTestData::DIGITS_CUSTOM,
  989. 'algorithm' => OtpTestData::ALGORITHM_CUSTOM,
  990. 'period' => null,
  991. 'counter' => OtpTestData::COUNTER_CUSTOM,
  992. ])
  993. ->assertJsonFragment([
  994. 'id' => 0,
  995. 'service' => OtpTestData::STEAM,
  996. 'account' => OtpTestData::ACCOUNT,
  997. 'otp_type' => 'steamtotp',
  998. 'secret' => OtpTestData::STEAM_SECRET,
  999. 'digits' => OtpTestData::DIGITS_STEAM,
  1000. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  1001. 'period' => OtpTestData::PERIOD_DEFAULT,
  1002. 'counter' => null,
  1003. ]);
  1004. }
  1005. /**
  1006. * Provide valid Plain Text files for import tests
  1007. */
  1008. public static function validPlainTextFileProvider()
  1009. {
  1010. return [
  1011. 'validPlainTextFile' => [
  1012. LocalFile::fake()->validPlainTextFile(),
  1013. ],
  1014. 'validPlainTextFileWithNewLines' => [
  1015. LocalFile::fake()->validPlainTextFileWithNewLines(),
  1016. ],
  1017. ];
  1018. }
  1019. #[Test]
  1020. #[DataProvider('invalidPlainTextFileProvider')]
  1021. public function test_migrate_invalid_plain_text_file_returns_bad_request($file)
  1022. {
  1023. $response = $this->withHeaders(['Content-Type' => 'multipart/form-data'])
  1024. ->actingAs($this->user, 'api-guard')
  1025. ->json('POST', '/api/v1/twofaccounts/migration', [
  1026. 'file' => $file,
  1027. ])
  1028. ->assertStatus(400);
  1029. }
  1030. /**
  1031. * Provide invalid Plain Text files for import tests
  1032. */
  1033. public static function invalidPlainTextFileProvider()
  1034. {
  1035. return [
  1036. 'invalidPlainTextFileEmpty' => [
  1037. LocalFile::fake()->invalidPlainTextFileEmpty(),
  1038. ],
  1039. 'invalidPlainTextFileNoUri' => [
  1040. LocalFile::fake()->invalidPlainTextFileNoUri(),
  1041. ],
  1042. 'invalidPlainTextFileWithInvalidUri' => [
  1043. LocalFile::fake()->invalidPlainTextFileWithInvalidUri(),
  1044. ],
  1045. 'invalidPlainTextFileWithInvalidLine' => [
  1046. LocalFile::fake()->invalidPlainTextFileWithInvalidLine(),
  1047. ],
  1048. ];
  1049. }
  1050. #[Test]
  1051. public function test_reorder_returns_success()
  1052. {
  1053. $response = $this->actingAs($this->user, 'api-guard')
  1054. ->json('POST', '/api/v1/twofaccounts/reorder', [
  1055. 'orderedIds' => [$this->twofaccountB->id, $this->twofaccountA->id],
  1056. ])
  1057. ->assertStatus(200)
  1058. ->assertJsonStructure([
  1059. 'message',
  1060. ]);
  1061. }
  1062. #[Test]
  1063. public function test_reorder_with_invalid_data_returns_validation_error()
  1064. {
  1065. $response = $this->actingAs($this->user, 'api-guard')
  1066. ->json('POST', '/api/v1/twofaccounts/reorder', [
  1067. 'orderedIds' => '3,2,1',
  1068. ])
  1069. ->assertStatus(422);
  1070. }
  1071. #[Test]
  1072. public function test_reorder_twofaccounts_of_another_user_is_forbidden()
  1073. {
  1074. $response = $this->actingAs($this->user, 'api-guard')
  1075. ->json('POST', '/api/v1/twofaccounts/reorder', [
  1076. 'orderedIds' => [$this->twofaccountB->id, $this->twofaccountD->id],
  1077. ])
  1078. ->assertForbidden()
  1079. ->assertJsonStructure([
  1080. 'message',
  1081. ]);
  1082. }
  1083. #[Test]
  1084. public function test_preview_returns_success_with_resource()
  1085. {
  1086. $response = $this->actingAs($this->user, 'api-guard')
  1087. ->json('POST', '/api/v1/twofaccounts/preview', [
  1088. 'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
  1089. ])
  1090. ->assertOk()
  1091. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  1092. }
  1093. #[Test]
  1094. public function test_preview_with_invalid_data_returns_validation_error()
  1095. {
  1096. $response = $this->actingAs($this->user, 'api-guard')
  1097. ->json('POST', '/api/v1/twofaccounts/preview', [
  1098. 'uri' => OtpTestData::INVALID_OTPAUTH_URI,
  1099. ])
  1100. ->assertStatus(422);
  1101. }
  1102. #[Test]
  1103. public function test_preview_with_unreachable_image_but_official_logo_returns_success()
  1104. {
  1105. $this->user['preferences->getOfficialIcons'] = true;
  1106. $response = $this->actingAs($this->user, 'api-guard')
  1107. ->json('POST', '/api/v1/twofaccounts/preview', [
  1108. 'uri' => OtpTestData::TOTP_URI_WITH_UNREACHABLE_IMAGE,
  1109. ])
  1110. ->assertOk();
  1111. $this->assertNotNull($response->json('icon'));
  1112. }
  1113. #[Test]
  1114. public function test_preview_with_unreachable_image_returns_success_with_no_icon()
  1115. {
  1116. $this->user['preferences->getOfficialIcons'] = false;
  1117. $response = $this->actingAs($this->user, 'api-guard')
  1118. ->json('POST', '/api/v1/twofaccounts/preview', [
  1119. 'uri' => OtpTestData::TOTP_URI_WITH_UNREACHABLE_IMAGE,
  1120. ])
  1121. ->assertOk()
  1122. ->assertJsonFragment([
  1123. 'icon' => null,
  1124. ]);
  1125. }
  1126. #[Test]
  1127. public function test_preview_with_infected_svg_image_stores_sanitized_image()
  1128. {
  1129. $this->user['preferences->getOfficialIcons'] = true;
  1130. $response = $this->actingAs($this->user, 'api-guard')
  1131. ->json('POST', '/api/v1/twofaccounts/preview', [
  1132. 'uri' => OtpTestData::TOTP_URI_WITH_INFECTED_SVG_IMAGE,
  1133. ])
  1134. ->assertOk();
  1135. $svgContent = IconStore::get($response->getData()->icon);
  1136. $this->assertStringNotContainsString(OtpTestData::ICON_SVG_MALICIOUS_CODE, $svgContent);
  1137. }
  1138. #[Test]
  1139. public function test_export_returns_json_migration_resource()
  1140. {
  1141. $this->twofaccountA = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  1142. $this->twofaccountB = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
  1143. $this->actingAs($this->user, 'api-guard')
  1144. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountB->id)
  1145. ->assertOk()
  1146. ->assertJsonStructure(self::VALID_EXPORT_STRUTURE)
  1147. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP)
  1148. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
  1149. }
  1150. #[Test]
  1151. public function test_export_returns_plain_text_with_otpauth_uris()
  1152. {
  1153. $this->twofaccountA = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  1154. $this->twofaccountB = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
  1155. $this->actingAs($this->user, 'api-guard')
  1156. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountB->id . '&otpauth=1')
  1157. ->assertOk()
  1158. ->assertJsonStructure(self::VALID_EXPORT_AS_URIS_STRUTURE)
  1159. ->assertJsonFragment(['uri' => $this->twofaccountA->getURI()])
  1160. ->assertJsonFragment(['uri' => $this->twofaccountB->getURI()]);
  1161. }
  1162. #[Test]
  1163. public function test_export_returns_json_migration_resource_when_otpauth_param_is_off()
  1164. {
  1165. $this->twofaccountA = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  1166. $this->twofaccountB = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
  1167. $this->actingAs($this->user, 'api-guard')
  1168. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountB->id . '&otpauth=0')
  1169. ->assertOk()
  1170. ->assertJsonStructure(self::VALID_EXPORT_STRUTURE)
  1171. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP)
  1172. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
  1173. }
  1174. #[Test]
  1175. public function test_export_too_many_ids_returns_bad_request()
  1176. {
  1177. TwoFAccount::factory()->count(102)->for($this->user)->create();
  1178. $ids = DB::table('twofaccounts')->where('user_id', $this->user->id)->pluck('id')->implode(',');
  1179. $response = $this->actingAs($this->user, 'api-guard')
  1180. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $ids)
  1181. ->assertStatus(400)
  1182. ->assertJsonStructure([
  1183. 'message',
  1184. 'reason',
  1185. ]);
  1186. }
  1187. #[Test]
  1188. public function test_export_missing_twofaccount_returns_existing_ones_only()
  1189. {
  1190. $this->twofaccountA = TwoFAccount::factory()->for($this->user)->create(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  1191. $response = $this->actingAs($this->user, 'api-guard')
  1192. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountA->id . ',1000')
  1193. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  1194. }
  1195. #[Test]
  1196. public function test_export_twofaccount_of_another_user_is_forbidden()
  1197. {
  1198. $response = $this->actingAs($this->user, 'api-guard')
  1199. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountC->id)
  1200. ->assertForbidden()
  1201. ->assertJsonStructure([
  1202. 'message',
  1203. ]);
  1204. }
  1205. #[Test]
  1206. public function test_export_returns_nulled_icon_resource_when_icon_file_is_missing()
  1207. {
  1208. $this->twofaccountA = TwoFAccount::factory()->for($this->user)->create(array_merge(
  1209. self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP,
  1210. [
  1211. 'icon' => 'icon_without_file_on_disk.png',
  1212. ]
  1213. ));
  1214. $response = $this->actingAs($this->user, 'api-guard')
  1215. ->json('GET', '/api/v1/twofaccounts/export?ids=' . $this->twofaccountA->id)
  1216. ->assertJsonFragment([
  1217. 'icon' => 'icon_without_file_on_disk.png',
  1218. 'icon_file' => null,
  1219. 'icon_mime' => null,
  1220. ]);
  1221. }
  1222. #[Test]
  1223. public function test_get_otp_using_totp_twofaccount_id_returns_consistent_resource()
  1224. {
  1225. $twofaccount = TwoFAccount::factory()->for($this->user)->create([
  1226. 'otp_type' => 'totp',
  1227. 'account' => OtpTestData::ACCOUNT,
  1228. 'service' => OtpTestData::SERVICE,
  1229. 'secret' => OtpTestData::SECRET,
  1230. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  1231. 'digits' => OtpTestData::DIGITS_DEFAULT,
  1232. 'period' => OtpTestData::PERIOD_DEFAULT,
  1233. 'legacy_uri' => OtpTestData::TOTP_SHORT_URI,
  1234. 'icon' => '',
  1235. ]);
  1236. $response = $this->actingAs($this->user, 'api-guard')
  1237. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '/otp')
  1238. ->assertOk()
  1239. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
  1240. ->assertJsonFragment([
  1241. 'otp_type' => 'totp',
  1242. 'period' => OtpTestData::PERIOD_DEFAULT,
  1243. ]);
  1244. }
  1245. #[Test]
  1246. public function test_get_otp_by_posting_totp_uri_returns_consistent_resource()
  1247. {
  1248. $response = $this->actingAs($this->user, 'api-guard')
  1249. ->json('POST', '/api/v1/twofaccounts/otp', [
  1250. 'uri' => OtpTestData::TOTP_FULL_CUSTOM_URI,
  1251. ])
  1252. ->assertOk()
  1253. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
  1254. ->assertJsonFragment([
  1255. 'otp_type' => 'totp',
  1256. 'period' => OtpTestData::PERIOD_CUSTOM,
  1257. ]);
  1258. }
  1259. #[Test]
  1260. public function test_get_otp_by_posting_totp_parameters_returns_consistent_resource()
  1261. {
  1262. $response = $this->actingAs($this->user, 'api-guard')
  1263. ->json('POST', '/api/v1/twofaccounts/otp', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  1264. ->assertOk()
  1265. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
  1266. ->assertJsonFragment([
  1267. 'otp_type' => 'totp',
  1268. 'period' => OtpTestData::PERIOD_CUSTOM,
  1269. ]);
  1270. }
  1271. #[Test]
  1272. public function test_get_otp_using_hotp_twofaccount_id_returns_consistent_resource()
  1273. {
  1274. $twofaccount = TwoFAccount::factory()->for($this->user)->create([
  1275. 'otp_type' => 'hotp',
  1276. 'account' => OtpTestData::ACCOUNT,
  1277. 'service' => OtpTestData::SERVICE,
  1278. 'secret' => OtpTestData::SECRET,
  1279. 'algorithm' => OtpTestData::ALGORITHM_DEFAULT,
  1280. 'digits' => OtpTestData::DIGITS_DEFAULT,
  1281. 'period' => null,
  1282. 'legacy_uri' => OtpTestData::HOTP_SHORT_URI,
  1283. 'icon' => '',
  1284. ]);
  1285. $response = $this->actingAs($this->user, 'api-guard')
  1286. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '/otp')
  1287. ->assertOk()
  1288. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
  1289. ->assertJsonFragment([
  1290. 'otp_type' => 'hotp',
  1291. 'counter' => OtpTestData::COUNTER_DEFAULT + 1,
  1292. ]);
  1293. }
  1294. #[Test]
  1295. public function test_get_otp_by_posting_hotp_uri_returns_consistent_resource()
  1296. {
  1297. $response = $this->actingAs($this->user, 'api-guard')
  1298. ->json('POST', '/api/v1/twofaccounts/otp', [
  1299. 'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
  1300. ])
  1301. ->assertOk()
  1302. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
  1303. ->assertJsonFragment([
  1304. 'otp_type' => 'hotp',
  1305. 'counter' => OtpTestData::COUNTER_CUSTOM + 1,
  1306. ]);
  1307. }
  1308. #[Test]
  1309. public function test_get_otp_by_posting_hotp_parameters_returns_consistent_resource()
  1310. {
  1311. $response = $this->actingAs($this->user, 'api-guard')
  1312. ->json('POST', '/api/v1/twofaccounts/otp', OtpTestData::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
  1313. ->assertOk()
  1314. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
  1315. ->assertJsonFragment([
  1316. 'otp_type' => 'hotp',
  1317. 'counter' => OtpTestData::COUNTER_CUSTOM + 1,
  1318. ]);
  1319. }
  1320. #[Test]
  1321. public function test_get_otp_by_posting_multiple_inputs_returns_bad_request()
  1322. {
  1323. $response = $this->actingAs($this->user, 'api-guard')
  1324. ->json('POST', '/api/v1/twofaccounts/otp', [
  1325. 'uri' => OtpTestData::HOTP_FULL_CUSTOM_URI,
  1326. 'key' => 'value',
  1327. ])
  1328. ->assertStatus(400)
  1329. ->assertJsonStructure([
  1330. 'message',
  1331. 'reason',
  1332. ]);
  1333. }
  1334. #[Test]
  1335. public function test_get_otp_using_indecipherable_twofaccount_id_returns_bad_request()
  1336. {
  1337. Settings::set('useEncryption', true);
  1338. $twofaccount = TwoFAccount::factory()->for($this->user)->create();
  1339. DB::table('twofaccounts')
  1340. ->where('id', $twofaccount->id)
  1341. ->update([
  1342. 'secret' => '**encrypted**',
  1343. ]);
  1344. $response = $this->actingAs($this->user, 'api-guard')
  1345. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '/otp')
  1346. ->assertStatus(400)
  1347. ->assertJsonStructure([
  1348. 'message',
  1349. ]);
  1350. }
  1351. #[Test]
  1352. public function test_get_otp_using_missing_twofaccount_id_returns_not_found()
  1353. {
  1354. $response = $this->actingAs($this->user, 'api-guard')
  1355. ->json('GET', '/api/v1/twofaccounts/1000/otp')
  1356. ->assertNotFound();
  1357. }
  1358. #[Test]
  1359. public function test_get_otp_by_posting_invalid_uri_returns_validation_error()
  1360. {
  1361. $response = $this->actingAs($this->user, 'api-guard')
  1362. ->json('POST', '/api/v1/twofaccounts/otp', [
  1363. 'uri' => OtpTestData::INVALID_OTPAUTH_URI,
  1364. ])
  1365. ->assertStatus(422);
  1366. }
  1367. #[Test]
  1368. public function test_get_otp_by_posting_invalid_parameters_returns_validation_error()
  1369. {
  1370. $response = $this->actingAs($this->user, 'api-guard')
  1371. ->json('POST', '/api/v1/twofaccounts/otp', self::ARRAY_OF_INVALID_PARAMETERS)
  1372. ->assertStatus(422);
  1373. }
  1374. #[Test]
  1375. public function test_get_otp_of_another_user_twofaccount_is_forbidden()
  1376. {
  1377. $response = $this->actingAs($this->user, 'api-guard')
  1378. ->json('GET', '/api/v1/twofaccounts/' . $this->twofaccountC->id . '/otp')
  1379. ->assertForbidden()
  1380. ->assertJsonStructure([
  1381. 'message',
  1382. ]);
  1383. }
  1384. #[Test]
  1385. public function test_count_returns_right_number_of_twofaccounts()
  1386. {
  1387. $response = $this->actingAs($this->user, 'api-guard')
  1388. ->json('GET', '/api/v1/twofaccounts/count')
  1389. ->assertStatus(200)
  1390. ->assertExactJson([
  1391. 'count' => 2,
  1392. ]);
  1393. }
  1394. #[Test]
  1395. public function test_withdraw_returns_success()
  1396. {
  1397. $response = $this->actingAs($this->user, 'api-guard')
  1398. ->json('PATCH', '/api/v1/twofaccounts/withdraw?ids=1,2')
  1399. ->assertOk()
  1400. ->assertJsonStructure([
  1401. 'message',
  1402. ]);
  1403. }
  1404. #[Test]
  1405. public function test_withdraw_too_many_ids_returns_bad_request()
  1406. {
  1407. TwoFAccount::factory()->count(102)->for($this->user)->create();
  1408. $ids = DB::table('twofaccounts')->where('user_id', $this->user->id)->pluck('id')->implode(',');
  1409. $response = $this->actingAs($this->user, 'api-guard')
  1410. ->json('PATCH', '/api/v1/twofaccounts/withdraw?ids=' . $ids)
  1411. ->assertStatus(400)
  1412. ->assertJsonStructure([
  1413. 'message',
  1414. 'reason',
  1415. ]);
  1416. }
  1417. #[Test]
  1418. public function test_destroy_twofaccount_returns_success()
  1419. {
  1420. $response = $this->actingAs($this->user, 'api-guard')
  1421. ->json('DELETE', '/api/v1/twofaccounts/' . $this->twofaccountA->id)
  1422. ->assertNoContent();
  1423. }
  1424. #[Test]
  1425. public function test_destroy_missing_twofaccount_returns_not_found()
  1426. {
  1427. $response = $this->actingAs($this->user, 'api-guard')
  1428. ->json('DELETE', '/api/v1/twofaccounts/1000')
  1429. ->assertNotFound();
  1430. }
  1431. #[Test]
  1432. public function test_destroy_twofaccount_of_another_user_is_forbidden()
  1433. {
  1434. $response = $this->actingAs($this->user, 'api-guard')
  1435. ->json('DELETE', '/api/v1/twofaccounts/' . $this->twofaccountC->id)
  1436. ->assertForbidden()
  1437. ->assertJsonStructure([
  1438. 'message',
  1439. ]);
  1440. }
  1441. #[Test]
  1442. public function test_batch_destroy_twofaccount_returns_success()
  1443. {
  1444. TwoFAccount::factory()->count(3)->for($this->user)->create();
  1445. $response = $this->actingAs($this->user, 'api-guard')
  1446. ->json('DELETE', '/api/v1/twofaccounts?ids=' . $this->twofaccountA->id . ',' . $this->twofaccountB->id)
  1447. ->assertNoContent();
  1448. }
  1449. #[Test]
  1450. public function test_batch_destroy_too_many_twofaccounts_returns_bad_request()
  1451. {
  1452. TwoFAccount::factory()->count(102)->for($this->user)->create();
  1453. $ids = DB::table('twofaccounts')->where('user_id', $this->user->id)->pluck('id')->implode(',');
  1454. $response = $this->actingAs($this->user, 'api-guard')
  1455. ->json('DELETE', '/api/v1/twofaccounts?ids=' . $ids)
  1456. ->assertStatus(400)
  1457. ->assertJsonStructure([
  1458. 'message',
  1459. 'reason',
  1460. ]);
  1461. }
  1462. #[Test]
  1463. public function test_batch_destroy_twofaccount_of_another_user_is_forbidden()
  1464. {
  1465. TwoFAccount::factory()->count(2)->for($this->anotherUser)->create();
  1466. $ids = DB::table('twofaccounts')
  1467. ->where('user_id', $this->anotherUser->id)
  1468. ->pluck('id')
  1469. ->implode(',');
  1470. $response = $this->actingAs($this->user, 'api-guard')
  1471. ->json('DELETE', '/api/v1/twofaccounts?ids=' . $ids)
  1472. ->assertForbidden()
  1473. ->assertJsonStructure([
  1474. 'message',
  1475. ]);
  1476. }
  1477. }