TwoFAccountControllerTest.php 57 KB

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