TwoFAccountControllerTest.php 56 KB

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