TwoFAccountControllerTest.php 35 KB

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