aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts')
-rw-r--r--packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts225
1 files changed, 225 insertions, 0 deletions
diff --git a/packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts b/packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts
new file mode 100644
index 0000000..c145bf3
--- /dev/null
+++ b/packages/main/src/reactions/__tests__/synchronizeConfig.spec.ts
@@ -0,0 +1,225 @@
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';
23import ms from 'ms';
24
25import type ConfigRepository from '../../infrastructure/config/ConfigRepository';
26import SharedStore from '../../stores/SharedStore';
27import type Disposer from '../../utils/Disposer';
28import { silenceLogger } from '../../utils/log';
29import synchronizeConfig from '../synchronizeConfig';
30
31let store: SharedStore;
32const repository: ConfigRepository = {
33 readConfig: jest.fn(),
34 writeConfig: jest.fn(),
35 watchConfig: jest.fn(),
36};
37const lessThanThrottleMs = ms('0.1s');
38const throttleMs = ms('1s');
39
40beforeAll(() => {
41 jest.useFakeTimers();
42 silenceLogger();
43});
44
45beforeEach(() => {
46 store = SharedStore.create();
47});
48
49describe('when synchronizeializing', () => {
50 describe('when there is no config file', () => {
51 beforeEach(() => {
52 mocked(repository.readConfig).mockResolvedValueOnce({
53 found: false,
54 });
55 });
56
57 it('should create a new config file', async () => {
58 await synchronizeConfig(store, repository);
59 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
60 });
61
62 it('should bail if there is an an error creating the config file', async () => {
63 mocked(repository.writeConfig).mockRejectedValue(new Error('boo'));
64 await expect(() =>
65 synchronizeConfig(store, repository),
66 ).rejects.toBeInstanceOf(Error);
67 });
68 });
69
70 describe('when there is a valid config file', () => {
71 beforeEach(() => {
72 mocked(repository.readConfig).mockResolvedValueOnce({
73 found: true,
74 data: {
75 // Use a default empty config file to not trigger config rewrite.
76 ...store.config,
77 themeSource: 'dark',
78 },
79 });
80 });
81
82 it('should read the existing config file is there is one', async () => {
83 await synchronizeConfig(store, repository);
84 expect(repository.writeConfig).not.toHaveBeenCalled();
85 expect(store.settings.themeSource).toBe('dark');
86 });
87
88 it('should bail if it cannot set up a watcher', async () => {
89 mocked(repository.watchConfig).mockImplementationOnce(() => {
90 throw new Error('boo');
91 });
92 await expect(() =>
93 synchronizeConfig(store, repository),
94 ).rejects.toBeInstanceOf(Error);
95 });
96 });
97
98 it('should update the config file if new details are added during read', async () => {
99 mocked(repository.readConfig).mockResolvedValueOnce({
100 found: true,
101 data: {
102 themeSource: 'light',
103 profile: {
104 name: 'Test profile',
105 },
106 },
107 });
108 await synchronizeConfig(store, repository);
109 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
110 });
111
112 it('should not apply an invalid config file but should not overwrite it', async () => {
113 mocked(repository.readConfig).mockResolvedValueOnce({
114 found: true,
115 data: {
116 themeSource: -1,
117 },
118 });
119 await synchronizeConfig(store, repository);
120 expect(store.settings.themeSource).not.toBe(-1);
121 expect(repository.writeConfig).not.toHaveBeenCalled();
122 });
123
124 it('should bail if it cannot determine whether there is a config file', async () => {
125 mocked(repository.readConfig).mockRejectedValue(new Error('boo'));
126 await expect(() =>
127 synchronizeConfig(store, repository),
128 ).rejects.toBeInstanceOf(Error);
129 });
130});
131
132describe('when it has loaded the config', () => {
133 let sutDisposer: Disposer;
134 const watcherDisposer: Disposer = jest.fn();
135 let configChangedCallback: () => Promise<void>;
136
137 beforeEach(async () => {
138 mocked(repository.readConfig).mockResolvedValueOnce({
139 found: true,
140 data: store.config,
141 });
142 mocked(repository.watchConfig).mockReturnValueOnce(watcherDisposer);
143 sutDisposer = await synchronizeConfig(store, repository, throttleMs);
144 [[configChangedCallback]] = mocked(repository.watchConfig).mock.calls;
145 jest.resetAllMocks();
146 });
147
148 it('should throttle saving changes to the config file', () => {
149 mocked(repository.writeConfig).mockResolvedValue();
150 store.settings.setThemeSource('dark');
151 jest.advanceTimersByTime(lessThanThrottleMs);
152 store.settings.setThemeSource('light');
153 jest.advanceTimersByTime(throttleMs);
154 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
155 });
156
157 it('should handle config writing errors gracefully', () => {
158 mocked(repository.writeConfig).mockRejectedValue(new Error('boo'));
159 store.settings.setThemeSource('dark');
160 jest.advanceTimersByTime(throttleMs);
161 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
162 });
163
164 it('should read the config file when it has changed', async () => {
165 mocked(repository.readConfig).mockResolvedValueOnce({
166 found: true,
167 data: {
168 // Use a default empty config file to not trigger config rewrite.
169 ...store.config,
170 themeSource: 'dark',
171 },
172 });
173 await configChangedCallback();
174 // Do not write back the changes we have just read.
175 expect(repository.writeConfig).not.toHaveBeenCalled();
176 expect(store.settings.themeSource).toBe('dark');
177 });
178
179 it('should update the config file if new details are added', async () => {
180 mocked(repository.readConfig).mockResolvedValueOnce({
181 found: true,
182 data: {
183 themeSource: 'light',
184 profile: {
185 name: 'Test profile',
186 },
187 },
188 });
189 await configChangedCallback();
190 expect(repository.writeConfig).toHaveBeenCalledTimes(1);
191 });
192
193 it('should not apply an invalid config file when it has changed but should not overwrite it', async () => {
194 mocked(repository.readConfig).mockResolvedValueOnce({
195 found: true,
196 data: {
197 themeSource: -1,
198 },
199 });
200 await configChangedCallback();
201 expect(store.settings.themeSource).not.toBe(-1);
202 expect(repository.writeConfig).not.toHaveBeenCalled();
203 });
204
205 it('should handle config reading errors gracefully', async () => {
206 mocked(repository.readConfig).mockRejectedValue(new Error('boo'));
207 await expect(configChangedCallback()).resolves.not.toThrow();
208 });
209
210 describe('when it was disposed', () => {
211 beforeEach(() => {
212 sutDisposer();
213 });
214
215 it('should dispose the watcher', () => {
216 expect(watcherDisposer).toHaveBeenCalled();
217 });
218
219 it('should not listen to store changes any more', () => {
220 store.settings.setThemeSource('dark');
221 jest.advanceTimersByTime(2 * throttleMs);
222 expect(repository.writeConfig).not.toHaveBeenCalled();
223 });
224 });
225});