TwoFAccountControllerTest.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. <?php
  2. namespace Tests\Api\v1\Controllers;
  3. use App\Models\User;
  4. use App\Models\Group;
  5. use Tests\FeatureTestCase;
  6. use App\Models\TwoFAccount;
  7. use Illuminate\Support\Facades\DB;
  8. use Illuminate\Support\Facades\Storage;
  9. /**
  10. * @covers \App\Api\v1\Controllers\TwoFAccountController
  11. * @covers \App\Api\v1\Resources\TwoFAccountReadResource
  12. * @covers \App\Api\v1\Resources\TwoFAccountStoreResource
  13. */
  14. class TwoFAccountControllerTest extends FeatureTestCase
  15. {
  16. /**
  17. * @var \App\Models\User
  18. */
  19. protected $user;
  20. /**
  21. * @var \App\Models\Group
  22. */
  23. protected $group;
  24. private const ACCOUNT = 'account';
  25. private const SERVICE = 'service';
  26. private const SECRET = 'A4GRFHVVRBGY7UIW';
  27. private const ALGORITHM_DEFAULT = 'sha1';
  28. private const ALGORITHM_CUSTOM = 'sha256';
  29. private const DIGITS_DEFAULT = 6;
  30. private const DIGITS_CUSTOM = 7;
  31. private const PERIOD_DEFAULT = 30;
  32. private const PERIOD_CUSTOM = 40;
  33. private const COUNTER_DEFAULT = 0;
  34. private const COUNTER_CUSTOM = 5;
  35. private const IMAGE = 'https%3A%2F%2Fen.opensuse.org%2Fimages%2F4%2F44%2FButton-filled-colour.png';
  36. private const ICON = 'test.png';
  37. private const TOTP_FULL_CUSTOM_URI = 'otpauth://totp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&period='.self::PERIOD_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
  38. private const HOTP_FULL_CUSTOM_URI = 'otpauth://hotp/'.self::SERVICE.':'.self::ACCOUNT.'?secret='.self::SECRET.'&issuer='.self::SERVICE.'&digits='.self::DIGITS_CUSTOM.'&counter='.self::COUNTER_CUSTOM.'&algorithm='.self::ALGORITHM_CUSTOM.'&image='.self::IMAGE;
  39. private const TOTP_SHORT_URI = 'otpauth://totp/'.self::ACCOUNT.'?secret='.self::SECRET;
  40. private const HOTP_SHORT_URI = 'otpauth://hotp/'.self::ACCOUNT.'?secret='.self::SECRET;
  41. private const TOTP_URI_WITH_UNREACHABLE_IMAGE = 'otpauth://totp/service:account?secret=A4GRFHVVRBGY7UIW&image=https%3A%2F%2Fen.opensuse.org%2Fimage.png';
  42. private const INVALID_OTPAUTH_URI = 'otpauth://Xotp/'.self::ACCOUNT.'?secret='.self::SECRET;
  43. private const VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET = [
  44. 'id',
  45. 'group_id',
  46. 'service',
  47. 'account',
  48. 'icon',
  49. 'otp_type',
  50. 'digits',
  51. 'algorithm',
  52. 'period',
  53. 'counter'
  54. ];
  55. private const VALID_RESOURCE_STRUCTURE_WITH_SECRET = [
  56. 'id',
  57. 'group_id',
  58. 'service',
  59. 'account',
  60. 'icon',
  61. 'otp_type',
  62. 'secret',
  63. 'digits',
  64. 'algorithm',
  65. 'period',
  66. 'counter'
  67. ];
  68. private const VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP = [
  69. 'generated_at',
  70. 'otp_type',
  71. 'password',
  72. 'period',
  73. ];
  74. private const VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP = [
  75. 'otp_type',
  76. 'password',
  77. 'counter',
  78. ];
  79. private const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP = [
  80. 'service' => self::SERVICE,
  81. 'account' => self::ACCOUNT,
  82. 'icon' => self::ICON,
  83. 'otp_type' => 'totp',
  84. 'secret' => self::SECRET,
  85. 'digits' => self::DIGITS_CUSTOM,
  86. 'algorithm' => self::ALGORITHM_CUSTOM,
  87. 'period' => self::PERIOD_CUSTOM,
  88. 'counter' => null,
  89. ];
  90. private const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP = [
  91. 'account' => self::ACCOUNT,
  92. 'otp_type' => 'totp',
  93. 'secret' => self::SECRET,
  94. ];
  95. private const JSON_FRAGMENTS_FOR_CUSTOM_TOTP = [
  96. 'service' => self::SERVICE,
  97. 'account' => self::ACCOUNT,
  98. 'otp_type' => 'totp',
  99. 'secret' => self::SECRET,
  100. 'digits' => self::DIGITS_CUSTOM,
  101. 'algorithm' => self::ALGORITHM_CUSTOM,
  102. 'period' => self::PERIOD_CUSTOM,
  103. 'counter' => null,
  104. ];
  105. private const JSON_FRAGMENTS_FOR_DEFAULT_TOTP = [
  106. 'service' => null,
  107. 'account' => self::ACCOUNT,
  108. 'otp_type' => 'totp',
  109. 'secret' => self::SECRET,
  110. 'digits' => self::DIGITS_DEFAULT,
  111. 'algorithm' => self::ALGORITHM_DEFAULT,
  112. 'period' => self::PERIOD_DEFAULT,
  113. 'counter' => null,
  114. ];
  115. private const ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP = [
  116. 'service' => self::SERVICE,
  117. 'account' => self::ACCOUNT,
  118. 'icon' => self::ICON,
  119. 'otp_type' => 'hotp',
  120. 'secret' => self::SECRET,
  121. 'digits' => self::DIGITS_CUSTOM,
  122. 'algorithm' => self::ALGORITHM_CUSTOM,
  123. 'period' => null,
  124. 'counter' => self::COUNTER_CUSTOM,
  125. ];
  126. private const ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP = [
  127. 'account' => self::ACCOUNT,
  128. 'otp_type' => 'hotp',
  129. 'secret' => self::SECRET,
  130. ];
  131. private const JSON_FRAGMENTS_FOR_CUSTOM_HOTP = [
  132. 'service' => self::SERVICE,
  133. 'account' => self::ACCOUNT,
  134. 'otp_type' => 'hotp',
  135. 'secret' => self::SECRET,
  136. 'digits' => self::DIGITS_CUSTOM,
  137. 'algorithm' => self::ALGORITHM_CUSTOM,
  138. 'period' => null,
  139. 'counter' => self::COUNTER_CUSTOM,
  140. ];
  141. private const JSON_FRAGMENTS_FOR_DEFAULT_HOTP = [
  142. 'service' => null,
  143. 'account' => self::ACCOUNT,
  144. 'otp_type' => 'hotp',
  145. 'secret' => self::SECRET,
  146. 'digits' => self::DIGITS_DEFAULT,
  147. 'algorithm' => self::ALGORITHM_DEFAULT,
  148. 'period' => null,
  149. 'counter' => self::COUNTER_DEFAULT,
  150. ];
  151. private const ARRAY_OF_INVALID_PARAMETERS = [
  152. 'account' => null,
  153. 'otp_type' => 'totp',
  154. 'secret' => self::SECRET,
  155. ];
  156. /**
  157. * @test
  158. */
  159. public function setUp(): void
  160. {
  161. parent::setUp();
  162. $this->user = User::factory()->create();
  163. $this->group = Group::factory()->create();
  164. }
  165. /**
  166. * @test
  167. */
  168. public function test_index_returns_twofaccount_collection()
  169. {
  170. TwoFAccount::factory()->count(3)->create();
  171. $response = $this->actingAs($this->user, 'api')
  172. ->json('GET', '/api/v1/twofaccounts')
  173. ->assertOk()
  174. ->assertJsonCount(3, $key = null)
  175. ->assertJsonStructure([
  176. '*' => self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET
  177. ]);
  178. }
  179. /**
  180. * @test
  181. */
  182. public function test_index_returns_twofaccount_collection_with_secret()
  183. {
  184. TwoFAccount::factory()->count(3)->create();
  185. $response = $this->actingAs($this->user, 'api')
  186. ->json('GET', '/api/v1/twofaccounts?withSecret=1')
  187. ->assertOk()
  188. ->assertJsonCount(3, $key = null)
  189. ->assertJsonStructure([
  190. '*' => self::VALID_RESOURCE_STRUCTURE_WITH_SECRET
  191. ]);
  192. }
  193. /**
  194. * @test
  195. */
  196. public function test_show_twofaccount_returns_twofaccount_resource_with_secret()
  197. {
  198. $twofaccount = TwoFAccount::factory()->create();
  199. $response = $this->actingAs($this->user, 'api')
  200. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id)
  201. ->assertOk()
  202. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_SECRET);
  203. }
  204. /**
  205. * @test
  206. */
  207. public function test_show_twofaccount_returns_twofaccount_resource_without_secret()
  208. {
  209. $twofaccount = TwoFAccount::factory()->create();
  210. $response = $this->actingAs($this->user, 'api')
  211. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '?withSecret=0')
  212. ->assertOk()
  213. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITHOUT_SECRET);
  214. }
  215. /**
  216. * @test
  217. */
  218. // public function test_show_twofaccount_with_indeciphered_data_returns_replaced_data()
  219. // {
  220. // $dbEncryptionService = resolve('App\Services\DbEncryptionService');
  221. // $dbEncryptionService->setTo(true);
  222. // $twofaccount = TwoFAccount::factory()->create();
  223. // DB::table('twofaccounts')
  224. // ->where('id', $twofaccount->id)
  225. // ->update([
  226. // 'secret' => '**encrypted**',
  227. // 'account' => '**encrypted**',
  228. // ]);
  229. // $response = $this->actingAs($this->user, 'api')
  230. // ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id)
  231. // ->assertJsonFragment([
  232. // 'secret' => '*indecipherable*',
  233. // 'account' => '*indecipherable*',
  234. // ]);
  235. // }
  236. /**
  237. * @test
  238. */
  239. public function test_show_missing_twofaccount_returns_not_found()
  240. {
  241. $response = $this->actingAs($this->user, 'api')
  242. ->json('GET', '/api/v1/twofaccounts/1000')
  243. ->assertNotFound()
  244. ->assertJsonStructure([
  245. 'message'
  246. ]);
  247. }
  248. /**
  249. * @dataProvider provideDataForTestStoreStructure
  250. * @test
  251. */
  252. public function test_store_returns_success_with_consistent_resource_structure(array $data)
  253. {
  254. Storage::put('test.png', 'emptied to prevent missing resource replaced by null by the model getter');
  255. $response = $this->actingAs($this->user, 'api')
  256. ->json('POST', '/api/v1/twofaccounts', $data)
  257. ->assertCreated()
  258. ->assertJsonStructure(self::VALID_RESOURCE_STRUCTURE_WITH_SECRET);
  259. }
  260. /**
  261. * Provide data for TwoFAccount store test
  262. */
  263. public function provideDataForTestStoreStructure() : array
  264. {
  265. return [
  266. [[
  267. 'uri' => self::TOTP_FULL_CUSTOM_URI,
  268. ]],
  269. [[
  270. 'uri' => self::TOTP_SHORT_URI,
  271. ]],
  272. [
  273. self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP
  274. ],
  275. [
  276. self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP
  277. ],
  278. [[
  279. 'uri' => self::HOTP_FULL_CUSTOM_URI,
  280. ]],
  281. [[
  282. 'uri' => self::HOTP_SHORT_URI,
  283. ]],
  284. [
  285. self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP
  286. ],
  287. [
  288. self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP
  289. ],
  290. ];
  291. }
  292. /**
  293. * @test
  294. */
  295. public function test_store_totp_using_fully_custom_uri_returns_consistent_resource()
  296. {
  297. $response = $this->actingAs($this->user, 'api')
  298. ->json('POST', '/api/v1/twofaccounts', [
  299. 'uri' => self::TOTP_FULL_CUSTOM_URI,
  300. ])
  301. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  302. }
  303. /**
  304. * @test
  305. */
  306. public function test_store_totp_using_short_uri_returns_resource_with_default_otp_parameter()
  307. {
  308. $response = $this->actingAs($this->user, 'api')
  309. ->json('POST', '/api/v1/twofaccounts', [
  310. 'uri' => self::TOTP_SHORT_URI,
  311. ])
  312. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  313. }
  314. /**
  315. * @test
  316. */
  317. public function test_store_totp_using_fully_custom_parameters_returns_consistent_resource()
  318. {
  319. $response = $this->actingAs($this->user, 'api')
  320. ->json('POST', '/api/v1/twofaccounts', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  321. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  322. }
  323. /**
  324. * @test
  325. */
  326. public function test_store_totp_using_minimum_parameters_returns_consistent_resource()
  327. {
  328. $response = $this->actingAs($this->user, 'api')
  329. ->json('POST', '/api/v1/twofaccounts', self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_TOTP)
  330. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_TOTP);
  331. }
  332. /**
  333. * @test
  334. */
  335. public function test_store_hotp_using_fully_custom_uri_returns_consistent_resource()
  336. {
  337. $response = $this->actingAs($this->user, 'api')
  338. ->json('POST', '/api/v1/twofaccounts', [
  339. 'uri' => self::HOTP_FULL_CUSTOM_URI,
  340. ])
  341. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP);
  342. }
  343. /**
  344. * @test
  345. */
  346. public function test_store_hotp_using_short_uri_returns_resource_with_default_otp_parameter()
  347. {
  348. $response = $this->actingAs($this->user, 'api')
  349. ->json('POST', '/api/v1/twofaccounts', [
  350. 'uri' => self::HOTP_SHORT_URI,
  351. ])
  352. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
  353. }
  354. /**
  355. * @test
  356. */
  357. public function test_store_hotp_using_fully_custom_parameters_returns_consistent_resource()
  358. {
  359. $response = $this->actingAs($this->user, 'api')
  360. ->json('POST', '/api/v1/twofaccounts', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
  361. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP);
  362. }
  363. /**
  364. * @test
  365. */
  366. public function test_store_hotp_using_minimum_parameters_returns_consistent_resource()
  367. {
  368. $response = $this->actingAs($this->user, 'api')
  369. ->json('POST', '/api/v1/twofaccounts', self::ARRAY_OF_MINIMUM_VALID_PARAMETERS_FOR_HOTP)
  370. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_DEFAULT_HOTP);
  371. }
  372. /**
  373. * @test
  374. */
  375. public function test_store_with_invalid_uri_returns_validation_error()
  376. {
  377. $response = $this->actingAs($this->user, 'api')
  378. ->json('POST', '/api/v1/twofaccounts', [
  379. 'uri' => self::INVALID_OTPAUTH_URI,
  380. ])
  381. ->assertStatus(422);
  382. }
  383. /**
  384. * @test
  385. */
  386. public function test_store_assigns_created_account_when_default_group_is_a_specific_one()
  387. {
  388. // Set the default group to a specific one
  389. $settingService = resolve('App\Services\SettingService');
  390. $settingService->set('defaultGroup', $this->group->id);
  391. $response = $this->actingAs($this->user, 'api')
  392. ->json('POST', '/api/v1/twofaccounts', [
  393. 'uri' => self::TOTP_SHORT_URI,
  394. ])
  395. ->assertJsonFragment([
  396. 'group_id' => $this->group->id
  397. ]);
  398. }
  399. /**
  400. * @test
  401. */
  402. public function test_store_assigns_created_account_when_default_group_is_the_active_one()
  403. {
  404. $settingService = resolve('App\Services\SettingService');
  405. // Set the default group to be the active one
  406. $settingService->set('defaultGroup', -1);
  407. // Set the active group
  408. $settingService->set('activeGroup', 1);
  409. $response = $this->actingAs($this->user, 'api')
  410. ->json('POST', '/api/v1/twofaccounts', [
  411. 'uri' => self::TOTP_SHORT_URI,
  412. ])
  413. ->assertJsonFragment([
  414. 'group_id' => 1
  415. ]);
  416. }
  417. /**
  418. * @test
  419. */
  420. public function test_store_assigns_created_account_when_default_group_is_no_group()
  421. {
  422. $settingService = resolve('App\Services\SettingService');
  423. // Set the default group to No group
  424. $settingService->set('defaultGroup', 0);
  425. $response = $this->actingAs($this->user, 'api')
  426. ->json('POST', '/api/v1/twofaccounts', [
  427. 'uri' => self::TOTP_SHORT_URI,
  428. ])
  429. ->assertJsonFragment([
  430. 'group_id' => null
  431. ]);
  432. }
  433. /**
  434. * @test
  435. */
  436. public function test_store_assigns_created_account_when_default_group_does_not_exist()
  437. {
  438. $settingService = resolve('App\Services\SettingService');
  439. // Set the default group to a non-existing one
  440. $settingService->set('defaultGroup', 1000);
  441. $response = $this->actingAs($this->user, 'api')
  442. ->json('POST', '/api/v1/twofaccounts', [
  443. 'uri' => self::TOTP_SHORT_URI,
  444. ])
  445. ->assertJsonFragment([
  446. 'group_id' => null
  447. ]);
  448. }
  449. /**
  450. * @test
  451. */
  452. public function test_update_totp_returns_success_with_updated_resource()
  453. {
  454. $twofaccount = TwoFAccount::factory()->create();
  455. $response = $this->actingAs($this->user, 'api')
  456. ->json('PUT', '/api/v1/twofaccounts/' . $twofaccount->id, self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  457. ->assertOk()
  458. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  459. }
  460. /**
  461. * @test
  462. */
  463. public function test_update_hotp_returns_success_with_updated_resource()
  464. {
  465. $twofaccount = TwoFAccount::factory()->create();
  466. $response = $this->actingAs($this->user, 'api')
  467. ->json('PUT', '/api/v1/twofaccounts/' . $twofaccount->id, self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
  468. ->assertOk()
  469. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_HOTP);
  470. }
  471. /**
  472. * @test
  473. */
  474. public function test_update_missing_twofaccount_returns_not_found()
  475. {
  476. $response = $this->actingAs($this->user, 'api')
  477. ->json('PUT', '/api/v1/twofaccounts/1000', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  478. ->assertNotFound();
  479. }
  480. /**
  481. * @test
  482. */
  483. public function test_update_twofaccount_with_invalid_data_returns_validation_error()
  484. {
  485. $twofaccount = TwoFAccount::factory()->create();
  486. $response = $this->actingAs($this->user, 'api')
  487. ->json('PUT', '/api/v1/twofaccounts/' . $twofaccount->id, self::ARRAY_OF_INVALID_PARAMETERS)
  488. ->assertStatus(422);
  489. }
  490. /**
  491. * @test
  492. */
  493. public function test_reorder_returns_success()
  494. {
  495. TwoFAccount::factory()->count(3)->create();
  496. $response = $this->actingAs($this->user, 'api')
  497. ->json('POST', '/api/v1/twofaccounts/reorder', [
  498. 'orderedIds' => [3,2,1]])
  499. ->assertStatus(200)
  500. ->assertJsonStructure([
  501. 'message'
  502. ]);
  503. }
  504. /**
  505. * @test
  506. */
  507. public function test_reorder_with_invalid_data_returns_validation_error()
  508. {
  509. TwoFAccount::factory()->count(3)->create();
  510. $response = $this->actingAs($this->user, 'api')
  511. ->json('POST', '/api/v1/twofaccounts/reorder', [
  512. 'orderedIds' => '3,2,1'])
  513. ->assertStatus(422);
  514. }
  515. /**
  516. * @test
  517. */
  518. public function test_preview_returns_success_with_resource()
  519. {
  520. $response = $this->actingAs($this->user, 'api')
  521. ->json('POST', '/api/v1/twofaccounts/preview', [
  522. 'uri' => self::TOTP_FULL_CUSTOM_URI,
  523. ])
  524. ->assertOk()
  525. ->assertJsonFragment(self::JSON_FRAGMENTS_FOR_CUSTOM_TOTP);
  526. }
  527. /**
  528. * @test
  529. */
  530. public function test_preview_with_invalid_data_returns_validation_error()
  531. {
  532. $response = $this->actingAs($this->user, 'api')
  533. ->json('POST', '/api/v1/twofaccounts/preview', [
  534. 'uri' => self::INVALID_OTPAUTH_URI,
  535. ])
  536. ->assertStatus(422);
  537. }
  538. /**
  539. * @test
  540. */
  541. public function test_preview_with_unreachable_image_returns_success()
  542. {
  543. $response = $this->actingAs($this->user, 'api')
  544. ->json('POST', '/api/v1/twofaccounts/preview', [
  545. 'uri' => self::TOTP_URI_WITH_UNREACHABLE_IMAGE,
  546. ])
  547. ->assertOk()
  548. ->assertJsonFragment([
  549. 'icon' => null
  550. ]);
  551. }
  552. /**
  553. * @test
  554. */
  555. public function test_get_otp_using_totp_twofaccount_id_returns_consistent_resource()
  556. {
  557. $twofaccount = TwoFAccount::factory()->create([
  558. 'otp_type' => 'totp',
  559. 'account' => self::ACCOUNT,
  560. 'service' => self::SERVICE,
  561. 'secret' => self::SECRET,
  562. 'algorithm' => self::ALGORITHM_DEFAULT,
  563. 'digits' => self::DIGITS_DEFAULT,
  564. 'period' => self::PERIOD_DEFAULT,
  565. 'legacy_uri' => self::TOTP_SHORT_URI,
  566. 'icon' => '',
  567. ]);
  568. $response = $this->actingAs($this->user, 'api')
  569. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '/otp')
  570. ->assertOk()
  571. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
  572. ->assertJsonFragment([
  573. 'otp_type' => 'totp',
  574. 'period' => self::PERIOD_DEFAULT,
  575. ]);
  576. }
  577. /**
  578. * @test
  579. */
  580. public function test_get_otp_by_posting_totp_uri_returns_consistent_resource()
  581. {
  582. $response = $this->actingAs($this->user, 'api')
  583. ->json('POST', '/api/v1/twofaccounts/otp', [
  584. 'uri' => self::TOTP_FULL_CUSTOM_URI,
  585. ])
  586. ->assertOk()
  587. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
  588. ->assertJsonFragment([
  589. 'otp_type' => 'totp',
  590. 'period' => self::PERIOD_CUSTOM,
  591. ]);
  592. }
  593. /**
  594. * @test
  595. */
  596. public function test_get_otp_by_posting_totp_parameters_returns_consistent_resource()
  597. {
  598. $response = $this->actingAs($this->user, 'api')
  599. ->json('POST', '/api/v1/twofaccounts/otp', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_TOTP)
  600. ->assertOk()
  601. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_TOTP)
  602. ->assertJsonFragment([
  603. 'otp_type' => 'totp',
  604. 'period' => self::PERIOD_CUSTOM,
  605. ]);
  606. }
  607. /**
  608. * @test
  609. */
  610. public function test_get_otp_using_hotp_twofaccount_id_returns_consistent_resource()
  611. {
  612. $twofaccount = TwoFAccount::factory()->create([
  613. 'otp_type' => 'hotp',
  614. 'account' => self::ACCOUNT,
  615. 'service' => self::SERVICE,
  616. 'secret' => self::SECRET,
  617. 'algorithm' => self::ALGORITHM_DEFAULT,
  618. 'digits' => self::DIGITS_DEFAULT,
  619. 'period' => null,
  620. 'legacy_uri' => self::HOTP_SHORT_URI,
  621. 'icon' => '',
  622. ]);
  623. $response = $this->actingAs($this->user, 'api')
  624. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '/otp')
  625. ->assertOk()
  626. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
  627. ->assertJsonFragment([
  628. 'otp_type' => 'hotp',
  629. 'counter' => self::COUNTER_DEFAULT + 1,
  630. ]);
  631. }
  632. /**
  633. * @test
  634. */
  635. public function test_get_otp_by_posting_hotp_uri_returns_consistent_resource()
  636. {
  637. $response = $this->actingAs($this->user, 'api')
  638. ->json('POST', '/api/v1/twofaccounts/otp', [
  639. 'uri' => self::HOTP_FULL_CUSTOM_URI,
  640. ])
  641. ->assertOk()
  642. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
  643. ->assertJsonFragment([
  644. 'otp_type' => 'hotp',
  645. 'counter' => self::COUNTER_CUSTOM + 1,
  646. ]);
  647. }
  648. /**
  649. * @test
  650. */
  651. public function test_get_otp_by_posting_hotp_parameters_returns_consistent_resource()
  652. {
  653. $response = $this->actingAs($this->user, 'api')
  654. ->json('POST', '/api/v1/twofaccounts/otp', self::ARRAY_OF_FULL_VALID_PARAMETERS_FOR_CUSTOM_HOTP)
  655. ->assertOk()
  656. ->assertJsonStructure(self::VALID_OTP_RESOURCE_STRUCTURE_FOR_HOTP)
  657. ->assertJsonFragment([
  658. 'otp_type' => 'hotp',
  659. 'counter' => self::COUNTER_CUSTOM + 1,
  660. ]);
  661. }
  662. /**
  663. * @test
  664. */
  665. public function test_get_otp_by_posting_multiple_inputs_returns_bad_request()
  666. {
  667. $response = $this->actingAs($this->user, 'api')
  668. ->json('POST', '/api/v1/twofaccounts/otp', [
  669. 'uri' => self::HOTP_FULL_CUSTOM_URI,
  670. 'key' => 'value',
  671. ])
  672. ->assertStatus(400)
  673. ->assertJsonStructure([
  674. 'message',
  675. 'reason',
  676. ]);
  677. }
  678. /**
  679. * @test
  680. */
  681. public function test_get_otp_using_indecipherable_twofaccount_id_returns_bad_request()
  682. {
  683. $settingService = resolve('App\Services\SettingService');
  684. $settingService->set('useEncryption', true);
  685. $twofaccount = TwoFAccount::factory()->create();
  686. DB::table('twofaccounts')
  687. ->where('id', $twofaccount->id)
  688. ->update([
  689. 'secret' => '**encrypted**',
  690. ]);
  691. $response = $this->actingAs($this->user, 'api')
  692. ->json('GET', '/api/v1/twofaccounts/' . $twofaccount->id . '/otp')
  693. ->assertStatus(400)
  694. ->assertJsonStructure([
  695. 'message',
  696. ]);
  697. }
  698. /**
  699. * @test
  700. */
  701. public function test_get_otp_using_missing_twofaccount_id_returns_not_found()
  702. {
  703. $response = $this->actingAs($this->user, 'api')
  704. ->json('GET', '/api/v1/twofaccounts/1000/otp')
  705. ->assertNotFound();
  706. }
  707. /**
  708. * @test
  709. */
  710. public function test_get_otp_by_posting_invalid_uri_returns_validation_error()
  711. {
  712. $response = $this->actingAs($this->user, 'api')
  713. ->json('POST', '/api/v1/twofaccounts/otp', [
  714. 'uri' => self::INVALID_OTPAUTH_URI,
  715. ])
  716. ->assertStatus(422);
  717. }
  718. /**
  719. * @test
  720. */
  721. public function test_get_otp_by_posting_invalid_parameters_returns_validation_error()
  722. {
  723. $response = $this->actingAs($this->user, 'api')
  724. ->json('POST', '/api/v1/twofaccounts/otp', self::ARRAY_OF_INVALID_PARAMETERS)
  725. ->assertStatus(422);
  726. }
  727. /**
  728. * @test
  729. */
  730. public function test_count_returns_right_number_of_twofaccount()
  731. {
  732. TwoFAccount::factory()->count(3)->create();
  733. $response = $this->actingAs($this->user, 'api')
  734. ->json('GET', '/api/v1/twofaccounts/count')
  735. ->assertStatus(200)
  736. ->assertExactJson([
  737. 'count' => 3
  738. ]);
  739. }
  740. /**
  741. * @test
  742. */
  743. public function test_withdraw_returns_success()
  744. {
  745. TwoFAccount::factory()->count(3)->create();
  746. $ids = DB::table('twofaccounts')->pluck('id')->implode(',');
  747. $response = $this->actingAs($this->user, 'api')
  748. ->json('PATCH', '/api/v1/twofaccounts/withdraw?ids=1,2,3' . $ids)
  749. ->assertOk()
  750. ->assertJsonStructure([
  751. 'message',
  752. ]);
  753. }
  754. /**
  755. * @test
  756. */
  757. public function test_withdraw_too_many_ids_returns_bad_request()
  758. {
  759. TwoFAccount::factory()->count(102)->create();
  760. $ids = DB::table('twofaccounts')->pluck('id')->implode(',');
  761. $response = $this->actingAs($this->user, 'api')
  762. ->json('PATCH', '/api/v1/twofaccounts/withdraw?ids=' . $ids)
  763. ->assertStatus(400)
  764. ->assertJsonStructure([
  765. 'message',
  766. 'reason',
  767. ]);
  768. }
  769. /**
  770. * @test
  771. */
  772. public function test_destroy_twofaccount_returns_success()
  773. {
  774. $twofaccount = TwoFAccount::factory()->create();
  775. $response = $this->actingAs($this->user, 'api')
  776. ->json('DELETE', '/api/v1/twofaccounts/' . $twofaccount->id)
  777. ->assertNoContent();
  778. }
  779. /**
  780. * @test
  781. */
  782. public function test_destroy_missing_twofaccount_returns_not_found()
  783. {
  784. $twofaccount = TwoFAccount::factory()->create();
  785. $response = $this->actingAs($this->user, 'api')
  786. ->json('DELETE', '/api/v1/twofaccounts/1000')
  787. ->assertNotFound();
  788. }
  789. /**
  790. * @test
  791. */
  792. public function test_batch_destroy_twofaccount_returns_success()
  793. {
  794. TwoFAccount::factory()->count(3)->create();
  795. $ids = DB::table('twofaccounts')->pluck('id')->implode(',');
  796. $response = $this->actingAs($this->user, 'api')
  797. ->json('DELETE', '/api/v1/twofaccounts?ids=' . $ids)
  798. ->assertNoContent();
  799. }
  800. /**
  801. * @test
  802. */
  803. public function test_batch_destroy_too_many_twofaccounts_returns_bad_request()
  804. {
  805. TwoFAccount::factory()->count(102)->create();
  806. $ids = DB::table('twofaccounts')->pluck('id')->implode(',');
  807. $response = $this->actingAs($this->user, 'api')
  808. ->json('DELETE', '/api/v1/twofaccounts?ids=' . $ids)
  809. ->assertStatus(400)
  810. ->assertJsonStructure([
  811. 'message',
  812. 'reason',
  813. ]);
  814. }
  815. }