aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/reactions/__tests__/synchronizeConfig.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src/reactions/__tests__/synchronizeConfig.test.ts')
-rw-r--r--packages/main/src/reactions/__tests__/synchronizeConfig.test.ts230
1 files changed, 230 insertions, 0 deletions
diff --git a/packages/main/src/reactions/__tests__/synchronizeConfig.test.ts b/packages/main/src/reactions/__tests__/synchronizeConfig.test.ts
new file mode 100644
index 0000000..ae3b59f
--- /dev/null
+++ b/packages/main/src/reactions/__tests__/synchronizeConfig.test.ts
@@ -0,0 +1,230 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import { jest } from '@jest/globals';
22import { mocked } from 'jest-mock';
23
24import type ConfigRepository from '../../infrastructure/config/ConfigRepository.js';
25import SharedStore from '../../stores/SharedStore.js';
26import type Disposer from '../../utils/Disposer.js';
27import synchronizeConfig, { serializeConfig } from '../synchronizeConfig.js';
28
29let store: SharedStore;
30const repository: ConfigRepository = {
31 readConfig: jest.fn<ConfigRepository['readConfig']>(),
32 writeConfig: jest.fn<ConfigRepository['writeConfig']>(),
33 watchConfig: jest.fn<ConfigRepository['watchConfig']>(),
34};
35const lessThanThrottleMs = 100;
36const throttleMs = 1000;
37
38beforeAll(() => {
39 jest.useFakeTimers();
40});
41
42beforeEach(() => {
43 store = SharedStore.create();
44});
45
46describe('when initializing', () => {
47 describe('when there is no config file', () => {
48 beforeEach(() => {
49 mocked(repository.readConfig).mockResolvedValueOnce({
50 found: false,
51 });
52 });
53
54 test('should create a new config file', async () => {
55 await synchronizeConfig(store, repository);
56 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
57 });
58
59 test('should bail if there is an an error creating the config file', async () => {
60 mocked(repository.writeConfig).mockRejectedValue(new Error('boo'));
61 await expect(() =>
62 synchronizeConfig(store, repository),
63 ).rejects.toBeInstanceOf(Error);
64 });
65 });
66
67 describe('when there is a valid config file', () => {
68 beforeEach(() => {
69 mocked(repository.readConfig).mockResolvedValueOnce({
70 found: true,
71 contents: serializeConfig({
72 // Use a default empty config file to not trigger config rewrite.
73 ...store.config,
74 themeSource: 'dark',
75 }),
76 });
77 });
78
79 test('should read the existing config file is there is one', async () => {
80 await synchronizeConfig(store, repository);
81 expect(repository.writeConfig).not.toHaveBeenCalled();
82 expect(store.settings.themeSource).toBe('dark');
83 });
84
85 test('should bail if it cannot set up a watcher', async () => {
86 mocked(repository.watchConfig).mockImplementationOnce(() => {
87 throw new Error('boo');
88 });
89 await expect(() =>
90 synchronizeConfig(store, repository),
91 ).rejects.toBeInstanceOf(Error);
92 });
93 });
94
95 test('should update the config file if new details are added during read', async () => {
96 mocked(repository.readConfig).mockResolvedValueOnce({
97 found: true,
98 contents: `{
99 "themeSource": "light",
100 "profiles": [
101 {
102 "name": "Test profile"
103 }
104 ]
105}
106`,
107 });
108 await synchronizeConfig(store, repository);
109 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
110 });
111
112 test('should not apply an invalid config file but should not overwrite it', async () => {
113 mocked(repository.readConfig).mockResolvedValueOnce({
114 found: true,
115 contents: `{
116 "themeSource": -1
117}
118`,
119 });
120 await synchronizeConfig(store, repository);
121 expect(store.settings.themeSource).not.toBe(-1);
122 expect(repository.writeConfig).not.toHaveBeenCalled();
123 });
124
125 test('should bail if it cannot determine whether there is a config file', async () => {
126 mocked(repository.readConfig).mockRejectedValue(new Error('boo'));
127 await expect(() =>
128 synchronizeConfig(store, repository),
129 ).rejects.toBeInstanceOf(Error);
130 });
131});
132
133describe('when it has loaded the config', () => {
134 let sutDisposer: Disposer;
135 const watcherDisposer: Disposer = jest.fn();
136 let configChangedCallback: () => Promise<void>;
137
138 beforeEach(async () => {
139 mocked(repository.readConfig).mockResolvedValueOnce({
140 found: true,
141 contents: serializeConfig(store.config),
142 });
143 mocked(repository.watchConfig).mockReturnValueOnce(watcherDisposer);
144 sutDisposer = await synchronizeConfig(store, repository, throttleMs);
145 [[configChangedCallback]] = mocked(repository.watchConfig).mock.calls;
146 jest.resetAllMocks();
147 });
148
149 test('should throttle saving changes to the config file', () => {
150 mocked(repository.writeConfig).mockResolvedValue();
151 store.settings.setThemeSource('dark');
152 jest.advanceTimersByTime(lessThanThrottleMs);
153 store.settings.setThemeSource('light');
154 jest.advanceTimersByTime(throttleMs);
155 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
156 });
157
158 test('should handle config writing errors gracefully', () => {
159 mocked(repository.writeConfig).mockRejectedValue(new Error('boo'));
160 store.settings.setThemeSource('dark');
161 jest.advanceTimersByTime(throttleMs);
162 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
163 });
164
165 test('should read the config file when it has changed', async () => {
166 mocked(repository.readConfig).mockResolvedValueOnce({
167 found: true,
168 contents: serializeConfig({
169 // Use a default empty config file to not trigger config rewrite.
170 ...store.config,
171 themeSource: 'dark',
172 }),
173 });
174 await configChangedCallback();
175 // Do not write back the changes we have just read.
176 expect(repository.writeConfig).not.toHaveBeenCalled();
177 expect(store.settings.themeSource).toBe('dark');
178 });
179
180 test('should update the config file if new details are added', async () => {
181 mocked(repository.readConfig).mockResolvedValueOnce({
182 found: true,
183 contents: `{
184 "themeSource": "light",
185 "profiles": [
186 {
187 "name": "Test profile"
188 }
189 ]
190}
191`,
192 });
193 await configChangedCallback();
194 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
195 });
196
197 test('should not apply an invalid config file when it has changed but should not overwrite it', async () => {
198 mocked(repository.readConfig).mockResolvedValueOnce({
199 found: true,
200 contents: `{
201 "themeSource": -1
202}
203`,
204 });
205 await configChangedCallback();
206 expect(store.settings.themeSource).not.toBe(-1);
207 expect(repository.writeConfig).not.toHaveBeenCalled();
208 });
209
210 test('should handle config reading errors gracefully', async () => {
211 mocked(repository.readConfig).mockRejectedValue(new Error('boo'));
212 await expect(configChangedCallback()).resolves.not.toThrow();
213 });
214
215 describe('when it was disposed', () => {
216 beforeEach(() => {
217 sutDisposer();
218 });
219
220 test('should dispose the watcher', () => {
221 expect(watcherDisposer).toHaveBeenCalled();
222 });
223
224 test('should not listen to store changes any more', () => {
225 store.settings.setThemeSource('dark');
226 jest.advanceTimersByTime(2 * throttleMs);
227 expect(repository.writeConfig).not.toHaveBeenCalled();
228 });
229 });
230});