TwoFAccountControllerTest.php 34 KB

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