TwoFAccountControllerTest.php 55 KB

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