SettingServiceTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <?php
  2. namespace Tests\Feature\Services;
  3. use App\Events\storeIconsInDatabaseSettingChanged;
  4. use App\Exceptions\FailedIconStoreDatabaseTogglingException;
  5. use App\Facades\IconStore;
  6. use App\Facades\Settings;
  7. use App\Models\Icon;
  8. use App\Models\TwoFAccount;
  9. use App\Services\IconStoreService;
  10. use App\Services\SettingService;
  11. use Illuminate\Support\Facades\Cache;
  12. use Illuminate\Support\Facades\Crypt;
  13. use Illuminate\Support\Facades\DB;
  14. use Illuminate\Support\Facades\Event;
  15. use Mockery\MockInterface;
  16. use PHPUnit\Framework\Attributes\CoversClass;
  17. use PHPUnit\Framework\Attributes\DataProvider;
  18. use PHPUnit\Framework\Attributes\Test;
  19. use Tests\Data\OtpTestData;
  20. use Tests\FeatureTestCase;
  21. /**
  22. * SettingServiceTest test class
  23. */
  24. #[CoversClass(SettingService::class)]
  25. #[CoversClass(Settings::class)]
  26. class SettingServiceTest extends FeatureTestCase
  27. {
  28. /**
  29. * App\Models\Group $groupOne, $groupTwo
  30. */
  31. protected $twofaccountOne;
  32. protected $twofaccountTwo;
  33. private const KEY = 'key';
  34. private const VALUE = 'value';
  35. private const SETTING_NAME = 'MySetting';
  36. private const SETTING_NAME_ALT = 'MySettingAlt';
  37. private const SETTING_VALUE_STRING = 'MyValue';
  38. private const SETTING_VALUE_TRUE_TRANSFORMED = '{{1}}';
  39. private const SETTING_VALUE_FALSE_TRANSFORMED = '{{}}';
  40. private const SETTING_VALUE_INT = 10;
  41. private const SETTING_VALUE_FLOAT = 10.5;
  42. private const ACCOUNT = 'account';
  43. private const SERVICE = 'service';
  44. private const SECRET = 'A4GRFHVVRBGY7UIW';
  45. private const ALGORITHM_CUSTOM = 'sha256';
  46. private const DIGITS_CUSTOM = 7;
  47. private const PERIOD_CUSTOM = 40;
  48. private const EXTERNAL_IMAGE_URL_ENCODED = 'https%3A%2F%2Fen.opensuse.org%2Fimages%2F4%2F44%2FButton-filled-colour.png';
  49. private const ICON = 'test.png';
  50. private const TOTP_FULL_CUSTOM_URI = 'otpauth://totp/' . self::SERVICE . ':' . self::ACCOUNT . '?secret=' . self::SECRET . '&issuer=' . self::SERVICE . '&digits=' . self::DIGITS_CUSTOM . '&period=' . self::PERIOD_CUSTOM . '&algorithm=' . self::ALGORITHM_CUSTOM . '&image=' . self::EXTERNAL_IMAGE_URL_ENCODED;
  51. public function setUp() : void
  52. {
  53. parent::setUp();
  54. $this->twofaccountOne = new TwoFAccount;
  55. $this->twofaccountOne->legacy_uri = self::TOTP_FULL_CUSTOM_URI;
  56. $this->twofaccountOne->service = self::SERVICE;
  57. $this->twofaccountOne->account = self::ACCOUNT;
  58. $this->twofaccountOne->icon = self::ICON;
  59. $this->twofaccountOne->otp_type = 'totp';
  60. $this->twofaccountOne->secret = self::SECRET;
  61. $this->twofaccountOne->digits = self::DIGITS_CUSTOM;
  62. $this->twofaccountOne->algorithm = self::ALGORITHM_CUSTOM;
  63. $this->twofaccountOne->period = self::PERIOD_CUSTOM;
  64. $this->twofaccountOne->counter = null;
  65. $this->twofaccountOne->save();
  66. $this->twofaccountTwo = new TwoFAccount;
  67. $this->twofaccountTwo->legacy_uri = self::TOTP_FULL_CUSTOM_URI;
  68. $this->twofaccountTwo->service = self::SERVICE;
  69. $this->twofaccountTwo->account = self::ACCOUNT;
  70. $this->twofaccountTwo->icon = self::ICON;
  71. $this->twofaccountTwo->otp_type = 'totp';
  72. $this->twofaccountTwo->secret = self::SECRET;
  73. $this->twofaccountTwo->digits = self::DIGITS_CUSTOM;
  74. $this->twofaccountTwo->algorithm = self::ALGORITHM_CUSTOM;
  75. $this->twofaccountTwo->period = self::PERIOD_CUSTOM;
  76. $this->twofaccountTwo->counter = null;
  77. $this->twofaccountTwo->save();
  78. }
  79. #[Test]
  80. public function test_get_string_setting_returns_correct_value()
  81. {
  82. Settings::set(self::SETTING_NAME, self::SETTING_VALUE_STRING);
  83. $this->assertEquals(self::SETTING_VALUE_STRING, Settings::get(self::SETTING_NAME));
  84. }
  85. #[Test]
  86. public function test_get_boolean_setting_returns_true()
  87. {
  88. Settings::set(self::SETTING_NAME, self::SETTING_VALUE_TRUE_TRANSFORMED);
  89. $this->assertEquals(true, Settings::get(self::SETTING_NAME));
  90. }
  91. #[Test]
  92. public function test_get_boolean_setting_returns_false()
  93. {
  94. Settings::set(self::SETTING_NAME, self::SETTING_VALUE_FALSE_TRANSFORMED);
  95. $this->assertEquals(false, Settings::get(self::SETTING_NAME));
  96. }
  97. #[Test]
  98. public function test_get_int_setting_returns_int()
  99. {
  100. Settings::set(self::SETTING_NAME, self::SETTING_VALUE_INT);
  101. $value = Settings::get(self::SETTING_NAME);
  102. $this->assertEquals(self::SETTING_VALUE_INT, $value);
  103. $this->assertIsInt($value);
  104. }
  105. #[Test]
  106. public function test_get_float_setting_returns_float()
  107. {
  108. Settings::set(self::SETTING_NAME, self::SETTING_VALUE_FLOAT);
  109. $value = Settings::get(self::SETTING_NAME);
  110. $this->assertEquals(self::SETTING_VALUE_FLOAT, $value);
  111. $this->assertIsFloat($value);
  112. }
  113. #[Test]
  114. public function test_all_returns_default_and_overloaded_settings()
  115. {
  116. $default_options = config('2fauth.settings');
  117. unset($default_options['lastRadarScan']);
  118. Settings::set(self::SETTING_NAME, self::SETTING_VALUE_STRING);
  119. $all = Settings::all()->toArray();
  120. $this->assertArrayHasKey(self::SETTING_NAME, $all);
  121. $this->assertEquals($all[self::SETTING_NAME], self::SETTING_VALUE_STRING);
  122. foreach ($default_options as $key => $val) {
  123. $this->assertArrayHasKey($key, $all);
  124. $this->assertEquals($all[$key], $val);
  125. }
  126. }
  127. #[Test]
  128. public function test_set_setting_persist_correct_value_in_db_and_cache()
  129. {
  130. $value = Settings::set(self::SETTING_NAME, self::SETTING_VALUE_STRING);
  131. $cached = Cache::get(SettingService::CACHE_ITEM_NAME); // returns a Collection
  132. $this->assertDatabaseHas('options', [
  133. self::KEY => self::SETTING_NAME,
  134. self::VALUE => self::SETTING_VALUE_STRING,
  135. ]);
  136. $this->assertEquals($cached->get(self::SETTING_NAME), self::SETTING_VALUE_STRING);
  137. }
  138. #[Test]
  139. public function test_set_useEncryption_On_encrypts_all_data()
  140. {
  141. Settings::set('useEncryption', true);
  142. $twofaccounts = DB::table('twofaccounts')->get();
  143. Icon::factory()->create();
  144. $icons = DB::table('icons')->get();
  145. $twofaccounts->each(function ($item, $key) {
  146. $this->assertEquals(self::ACCOUNT, Crypt::decryptString($item->account));
  147. $this->assertEquals(self::SECRET, Crypt::decryptString($item->secret));
  148. $this->assertEquals(self::TOTP_FULL_CUSTOM_URI, Crypt::decryptString($item->legacy_uri));
  149. });
  150. $icons->each(function ($item, $key) {
  151. $this->assertEquals(OtpTestData::ICON_PNG_DATA, Crypt::decryptString($item->content));
  152. });
  153. }
  154. #[Test]
  155. public function test_set_useEncryption_On_twice_prevents_successive_encryption()
  156. {
  157. Settings::set('useEncryption', true);
  158. Settings::set('useEncryption', true);
  159. $twofaccounts = DB::table('twofaccounts')->get();
  160. Icon::factory()->create();
  161. $icons = DB::table('icons')->get();
  162. $twofaccounts->each(function ($item, $key) {
  163. $this->assertEquals(self::ACCOUNT, Crypt::decryptString($item->account));
  164. $this->assertEquals(self::SECRET, Crypt::decryptString($item->secret));
  165. $this->assertEquals(self::TOTP_FULL_CUSTOM_URI, Crypt::decryptString($item->legacy_uri));
  166. });
  167. $icons->each(function ($item, $key) {
  168. $this->assertEquals(OtpTestData::ICON_PNG_DATA, Crypt::decryptString($item->content));
  169. });
  170. }
  171. #[Test]
  172. public function test_set_useEncryption_Off_decrypts_all_accounts()
  173. {
  174. Settings::set('useEncryption', true);
  175. Settings::set('useEncryption', false);
  176. $twofaccounts = DB::table('twofaccounts')->get();
  177. Icon::factory()->create();
  178. $icons = DB::table('icons')->get();
  179. $twofaccounts->each(function ($item, $key) {
  180. $this->assertEquals(self::ACCOUNT, $item->account);
  181. $this->assertEquals(self::SECRET, $item->secret);
  182. $this->assertEquals(self::TOTP_FULL_CUSTOM_URI, $item->legacy_uri);
  183. });
  184. $icons->each(function ($item, $key) {
  185. $this->assertEquals(OtpTestData::ICON_PNG_DATA, $item->content);
  186. });
  187. }
  188. #[Test]
  189. #[DataProvider('provideUndecipherableData')]
  190. public function test_set_useEncryption_Off_returns_exception_when_data_are_undecipherable(array $data)
  191. {
  192. $this->expectException(\App\Exceptions\DbEncryptionException::class);
  193. Settings::set('useEncryption', true);
  194. $affected = DB::table('twofaccounts')
  195. ->where('id', $this->twofaccountOne->id)
  196. ->update($data);
  197. Settings::set('useEncryption', false);
  198. $twofaccount = TwoFAccount::find($this->twofaccountOne->id);
  199. }
  200. /**
  201. * Provide invalid data for validation test
  202. */
  203. public static function provideUndecipherableData() : array
  204. {
  205. return [
  206. [[
  207. 'account' => 'undecipherableString',
  208. ]],
  209. [[
  210. 'secret' => 'undecipherableString',
  211. ]],
  212. [[
  213. 'legacy_uri' => 'undecipherableString',
  214. ]],
  215. ];
  216. }
  217. #[Test]
  218. public function test_set_true_setting_persist_transformed_boolean()
  219. {
  220. $value = Settings::set(self::SETTING_NAME, true);
  221. $this->assertDatabaseHas('options', [
  222. self::KEY => self::SETTING_NAME,
  223. self::VALUE => self::SETTING_VALUE_TRUE_TRANSFORMED,
  224. ]);
  225. }
  226. #[Test]
  227. public function test_set_false_setting_persist_transformed_boolean()
  228. {
  229. $value = Settings::set(self::SETTING_NAME, false);
  230. $this->assertDatabaseHas('options', [
  231. self::KEY => self::SETTING_NAME,
  232. self::VALUE => self::SETTING_VALUE_FALSE_TRANSFORMED,
  233. ]);
  234. }
  235. #[Test]
  236. public function test_del_remove_setting_from_db_and_cache()
  237. {
  238. DB::table('options')->insert(
  239. [self::KEY => self::SETTING_NAME, self::VALUE => strval(self::SETTING_VALUE_STRING)]
  240. );
  241. Settings::delete(self::SETTING_NAME);
  242. $cached = Cache::get(SettingService::CACHE_ITEM_NAME); // returns a Collection
  243. $this->assertDatabaseMissing('options', [
  244. self::KEY => self::SETTING_NAME,
  245. self::VALUE => self::SETTING_VALUE_STRING,
  246. ]);
  247. $this->assertFalse($cached->has(self::SETTING_NAME));
  248. }
  249. #[Test]
  250. public function test_isEdited_returns_true()
  251. {
  252. DB::table('options')->insert(
  253. [self::KEY => 'showOtpAsDot', self::VALUE => strval(self::SETTING_VALUE_TRUE_TRANSFORMED)]
  254. );
  255. $this->assertTrue(Settings::isEdited('showOtpAsDot'));
  256. }
  257. #[Test]
  258. public function test_isEdited_returns_false()
  259. {
  260. DB::table('options')->where(self::KEY, 'showOtpAsDot')->delete();
  261. $this->assertFalse(Settings::isEdited('showOtpAsDot'));
  262. }
  263. #[Test]
  264. public function test_cache_is_requested_at_instanciation()
  265. {
  266. Cache::shouldReceive('remember')
  267. ->andReturn(collect([]));
  268. $settingService = new SettingService;
  269. Cache::shouldHaveReceived('remember');
  270. }
  271. #[Test]
  272. public function test_cache_is_updated_when_setting_is_set()
  273. {
  274. Cache::shouldReceive('remember', 'put')
  275. ->andReturn(collect([]), true);
  276. $settingService = new SettingService;
  277. $settingService->set(self::SETTING_NAME, self::SETTING_VALUE_STRING);
  278. Cache::shouldHaveReceived('put');
  279. }
  280. #[Test]
  281. public function test_cache_is_updated_when_setting_is_deleted()
  282. {
  283. Cache::shouldReceive('remember', 'put')
  284. ->andReturn(collect([]), true);
  285. $settingService = new SettingService;
  286. $settingService->delete(self::SETTING_NAME);
  287. Cache::shouldHaveReceived('put');
  288. }
  289. #[Test]
  290. public function test_set_storeIconsInDatabase_setting_dispatches_storeIconsInDatabaseSettingChanged()
  291. {
  292. Event::fake([
  293. storeIconsInDatabaseSettingChanged::class,
  294. ]);
  295. Settings::set('storeIconsInDatabase', true);
  296. Event::assertDispatched(storeIconsInDatabaseSettingChanged::class);
  297. }
  298. #[Test]
  299. public function test_set_storeIconsInDatabase_setting_impacts_the_icon_store()
  300. {
  301. Settings::set('storeIconsInDatabase', false);
  302. $this->assertFalse(IconStore::usesDatabase());
  303. Settings::set('storeIconsInDatabase', true);
  304. $this->assertTrue(IconStore::usesDatabase());
  305. }
  306. #[Test]
  307. public function test_set_storeIconsInDatabase_is_cancelled_if_database_toggling_failed()
  308. {
  309. $this->expectException(FailedIconStoreDatabaseTogglingException::class);
  310. $newValue = true;
  311. IconStore::shouldReceive('setDatabaseReplication')
  312. ->once()
  313. ->with($newValue)
  314. ->andThrow(FailedIconStoreDatabaseTogglingException::class);
  315. Settings::set('storeIconsInDatabase', $newValue);
  316. $this->assertFalse(Settings::get('storeIconsInDatabase'));
  317. }
  318. }